spikard 0.8.3 → 0.10.2
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 +2 -2
- 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 +3 -6
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +2 -2
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +4 -4
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +3 -3
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +10 -5
- 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 +11 -11
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +9 -37
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +436 -3
- data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +4 -4
- 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 +3 -3
- data/vendor/crates/spikard-core/src/di/container.rs +1 -1
- data/vendor/crates/spikard-core/src/di/factory.rs +2 -2
- data/vendor/crates/spikard-core/src/di/resolved.rs +2 -2
- data/vendor/crates/spikard-core/src/di/value.rs +1 -1
- data/vendor/crates/spikard-core/src/http.rs +75 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +43 -43
- data/vendor/crates/spikard-core/src/parameters.rs +14 -19
- data/vendor/crates/spikard-core/src/problem.rs +1 -1
- data/vendor/crates/spikard-core/src/request_data.rs +7 -16
- data/vendor/crates/spikard-core/src/router.rs +6 -0
- data/vendor/crates/spikard-core/src/schema_registry.rs +2 -3
- data/vendor/crates/spikard-core/src/type_hints.rs +3 -2
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +1 -1
- data/vendor/crates/spikard-core/src/validation/mod.rs +1 -1
- 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 +4 -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 +3 -1
- 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 +444 -62
- data/vendor/crates/spikard-rb/src/lifecycle.rs +29 -1
- 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 +1 -1
- 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
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
//! Response interpretation and handling for language bindings
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides traits and types for abstracting over language-specific
|
|
4
|
+
//! response patterns, enabling consistent handling across all bindings
|
|
5
|
+
//! (Python, Node.js, Ruby, PHP, WASM).
|
|
6
|
+
//!
|
|
7
|
+
//! # Overview
|
|
8
|
+
//!
|
|
9
|
+
//! Language bindings receive responses from handler functions that can take three forms:
|
|
10
|
+
//! 1. **Streaming responses** - Iterables that yield chunks incrementally
|
|
11
|
+
//! 2. **Custom responses** - Objects with explicit status codes, headers, and bodies
|
|
12
|
+
//! 3. **Plain responses** - Simple JSON values that become 200 OK responses
|
|
13
|
+
//!
|
|
14
|
+
//! Rather than duplicating response detection logic in each binding, this module
|
|
15
|
+
//! provides a shared [`ResponseInterpreter`] trait that bindings implement to translate
|
|
16
|
+
//! language-specific values into a unified [`InterpretedResponse`] enum.
|
|
17
|
+
//!
|
|
18
|
+
//! # Benefits
|
|
19
|
+
//!
|
|
20
|
+
//! - **Code reuse**: ~150 LOC of response detection logic shared across all bindings
|
|
21
|
+
//! - **Consistency**: All bindings interpret responses identically
|
|
22
|
+
//! - **Maintainability**: Single source of truth for response patterns
|
|
23
|
+
//! - **Extensibility**: Adding new response types requires changes in one place
|
|
24
|
+
//!
|
|
25
|
+
//! # Architecture
|
|
26
|
+
//!
|
|
27
|
+
//! ```text
|
|
28
|
+
//! Handler Function (Language-specific)
|
|
29
|
+
//! ↓
|
|
30
|
+
//! Language Value (Python object, Node object, etc.)
|
|
31
|
+
//! ↓
|
|
32
|
+
//! [ResponseInterpreter impl for language] — interprets language value
|
|
33
|
+
//! ↓
|
|
34
|
+
//! InterpretedResponse — unified enum (streaming, custom, or plain)
|
|
35
|
+
//! ↓
|
|
36
|
+
//! HTTP Response (status, headers, body)
|
|
37
|
+
//! ```
|
|
38
|
+
//!
|
|
39
|
+
//! # Examples
|
|
40
|
+
//!
|
|
41
|
+
//! ## Implementing `ResponseInterpreter` for a Language Binding
|
|
42
|
+
//!
|
|
43
|
+
//! ```ignore
|
|
44
|
+
//! use spikard_bindings_shared::{ResponseInterpreter, InterpretedResponse, StreamSource};
|
|
45
|
+
//! use serde_json::Value;
|
|
46
|
+
//! use std::collections::HashMap;
|
|
47
|
+
//!
|
|
48
|
+
//! struct PythonInterpreter;
|
|
49
|
+
//!
|
|
50
|
+
//! impl ResponseInterpreter for PythonInterpreter {
|
|
51
|
+
//! type LanguageValue = PyObject;
|
|
52
|
+
//! type Error = PyErr;
|
|
53
|
+
//!
|
|
54
|
+
//! fn is_streaming(&self, value: &Self::LanguageValue) -> bool {
|
|
55
|
+
//! // Check if object is iterable/generator
|
|
56
|
+
//! // e.g., hasattr(obj, '__iter__') and not isinstance(obj, dict)
|
|
57
|
+
//! todo!()
|
|
58
|
+
//! }
|
|
59
|
+
//!
|
|
60
|
+
//! fn is_custom_response(&self, value: &Self::LanguageValue) -> bool {
|
|
61
|
+
//! // Check if object has 'status_code' or 'headers' attributes
|
|
62
|
+
//! todo!()
|
|
63
|
+
//! }
|
|
64
|
+
//!
|
|
65
|
+
//! fn interpret(&self, value: &Self::LanguageValue) -> Result<InterpretedResponse, Self::Error> {
|
|
66
|
+
//! if self.is_streaming(value) {
|
|
67
|
+
//! // Wrap Python iterator in StreamSource
|
|
68
|
+
//! let stream = PythonStreamSource { obj: value.clone() };
|
|
69
|
+
//! Ok(InterpretedResponse::Streaming {
|
|
70
|
+
//! enumerator: Box::new(stream),
|
|
71
|
+
//! status: 200,
|
|
72
|
+
//! headers: HashMap::new(),
|
|
73
|
+
//! })
|
|
74
|
+
//! } else if self.is_custom_response(value) {
|
|
75
|
+
//! // Extract status, headers, body from custom response object
|
|
76
|
+
//! let status = extract_status(value)?;
|
|
77
|
+
//! let headers = extract_headers(value)?;
|
|
78
|
+
//! let body = extract_body(value)?;
|
|
79
|
+
//! Ok(InterpretedResponse::Custom {
|
|
80
|
+
//! status,
|
|
81
|
+
//! headers,
|
|
82
|
+
//! body,
|
|
83
|
+
//! raw_body: None,
|
|
84
|
+
//! })
|
|
85
|
+
//! } else {
|
|
86
|
+
//! // Treat as plain JSON
|
|
87
|
+
//! let body = python_to_json(value)?;
|
|
88
|
+
//! Ok(InterpretedResponse::Plain { body })
|
|
89
|
+
//! }
|
|
90
|
+
//! }
|
|
91
|
+
//! }
|
|
92
|
+
//! ```
|
|
93
|
+
|
|
94
|
+
use serde_json::Value;
|
|
95
|
+
use std::collections::HashMap;
|
|
96
|
+
|
|
97
|
+
/// A trait for language-specific response stream sources
|
|
98
|
+
///
|
|
99
|
+
/// This trait abstracts over different language iteration mechanisms
|
|
100
|
+
/// (Python generators, Node.js async iterables, Ruby enumerables, etc.)
|
|
101
|
+
/// to provide a uniform interface for streaming responses.
|
|
102
|
+
///
|
|
103
|
+
/// # Design Goals
|
|
104
|
+
///
|
|
105
|
+
/// - **Object-safe**: No generic type parameters, supports dynamic dispatch via `Box<dyn StreamSource>`
|
|
106
|
+
/// - **Chunk-oriented**: Returns `Option<Vec<u8>>` for each iteration, `None` on completion
|
|
107
|
+
/// - **Memory-efficient**: Yields data incrementally without buffering entire response
|
|
108
|
+
/// - **Async-compatible**: `Send + Sync` allows usage with async runtimes
|
|
109
|
+
///
|
|
110
|
+
/// # Implementation Notes
|
|
111
|
+
///
|
|
112
|
+
/// - Must be stateful: tracks position in the underlying iterator/generator
|
|
113
|
+
/// - Should handle language-specific iteration protocol (e.g., Python's `__next__`)
|
|
114
|
+
/// - Must propagate errors from the underlying iterator
|
|
115
|
+
/// - Should handle encoding (binary data or UTF-8 conversion as needed)
|
|
116
|
+
///
|
|
117
|
+
/// # Examples
|
|
118
|
+
///
|
|
119
|
+
/// ```ignore
|
|
120
|
+
/// // Python generator implementation
|
|
121
|
+
/// struct PythonStreamSource {
|
|
122
|
+
/// generator: PyObject,
|
|
123
|
+
/// py: Python<'static>,
|
|
124
|
+
/// }
|
|
125
|
+
///
|
|
126
|
+
/// impl StreamSource for PythonStreamSource {
|
|
127
|
+
/// fn next_chunk(&mut self) -> Option<Vec<u8>> {
|
|
128
|
+
/// // Call next() on Python generator
|
|
129
|
+
/// // Convert chunk to Vec<u8>
|
|
130
|
+
/// // Return None when StopIteration raised
|
|
131
|
+
/// todo!()
|
|
132
|
+
/// }
|
|
133
|
+
/// }
|
|
134
|
+
/// ```
|
|
135
|
+
pub trait StreamSource: Send + Sync {
|
|
136
|
+
/// Get the next chunk of data from the stream
|
|
137
|
+
///
|
|
138
|
+
/// # Returns
|
|
139
|
+
///
|
|
140
|
+
/// - `Some(chunk)` - A chunk of data (may be empty `vec![]` for flush signals)
|
|
141
|
+
/// - `None` - End of stream reached
|
|
142
|
+
///
|
|
143
|
+
/// # Errors
|
|
144
|
+
///
|
|
145
|
+
/// Errors from the underlying iterator should be handled by converting
|
|
146
|
+
/// to UTF-8 encoded error messages in the chunk stream, or by returning
|
|
147
|
+
/// `None` to signal end of stream and allowing the binding to handle errors
|
|
148
|
+
/// before streaming begins.
|
|
149
|
+
fn next_chunk(&mut self) -> Option<Vec<u8>>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Represents an interpreted HTTP response from a handler function
|
|
153
|
+
///
|
|
154
|
+
/// This enum captures the three possible response patterns:
|
|
155
|
+
/// 1. **Streaming** - Iterative data delivery
|
|
156
|
+
/// 2. **Custom** - Explicit control over status, headers, and body
|
|
157
|
+
/// 3. **Plain** - Simple JSON response
|
|
158
|
+
///
|
|
159
|
+
/// # Semantics
|
|
160
|
+
///
|
|
161
|
+
/// - **Streaming**: Used for server-sent events, file downloads, large responses
|
|
162
|
+
/// - Status code and headers are sent immediately
|
|
163
|
+
/// - Chunks are sent as they become available
|
|
164
|
+
/// - HTTP connection remains open until `StreamSource::next_chunk()` returns `None`
|
|
165
|
+
///
|
|
166
|
+
/// - **Custom**: Used for responses with explicit status codes or headers
|
|
167
|
+
/// - `status` is required (default 200 if not specified)
|
|
168
|
+
/// - `headers` can be empty for default behavior
|
|
169
|
+
/// - `body` is the JSON response body (may be `None` for streaming custom responses)
|
|
170
|
+
/// - `raw_body` can contain pre-encoded bytes (skips JSON serialization)
|
|
171
|
+
///
|
|
172
|
+
/// - **Plain**: Simplest form, always 200 OK
|
|
173
|
+
/// - `body` is a `Value` that will be JSON-serialized
|
|
174
|
+
/// - No custom headers or status codes
|
|
175
|
+
/// - Most common response type (~90% of real-world APIs)
|
|
176
|
+
///
|
|
177
|
+
/// # Performance Characteristics
|
|
178
|
+
///
|
|
179
|
+
/// - **Streaming**: Zero-copy streaming, minimal memory overhead
|
|
180
|
+
/// - **Custom**: One allocation for headers map
|
|
181
|
+
/// - **Plain**: One allocation for `Value`
|
|
182
|
+
///
|
|
183
|
+
/// # Examples
|
|
184
|
+
///
|
|
185
|
+
/// ```ignore
|
|
186
|
+
/// use spikard_bindings_shared::InterpretedResponse;
|
|
187
|
+
/// use serde_json::json;
|
|
188
|
+
/// use std::collections::HashMap;
|
|
189
|
+
///
|
|
190
|
+
/// // Streaming response
|
|
191
|
+
/// let response = InterpretedResponse::Streaming {
|
|
192
|
+
/// enumerator: Box::new(my_stream),
|
|
193
|
+
/// status: 200,
|
|
194
|
+
/// headers: Default::default(),
|
|
195
|
+
/// };
|
|
196
|
+
///
|
|
197
|
+
/// // Custom response with status and headers
|
|
198
|
+
/// let mut headers = HashMap::new();
|
|
199
|
+
/// headers.insert("content-type".to_string(), "application/json".to_string());
|
|
200
|
+
/// let response = InterpretedResponse::Custom {
|
|
201
|
+
/// status: 201,
|
|
202
|
+
/// headers,
|
|
203
|
+
/// body: Some(json!({ "id": 42 })),
|
|
204
|
+
/// raw_body: None,
|
|
205
|
+
/// };
|
|
206
|
+
///
|
|
207
|
+
/// // Plain JSON response
|
|
208
|
+
/// let response = InterpretedResponse::Plain {
|
|
209
|
+
/// body: json!({ "success": true }),
|
|
210
|
+
/// };
|
|
211
|
+
/// ```
|
|
212
|
+
pub enum InterpretedResponse {
|
|
213
|
+
/// Streaming response with incremental data delivery
|
|
214
|
+
///
|
|
215
|
+
/// Used for responses where data is produced over time:
|
|
216
|
+
/// - File downloads
|
|
217
|
+
/// - Server-sent events
|
|
218
|
+
/// - Large data sets
|
|
219
|
+
/// - Real-time data feeds
|
|
220
|
+
///
|
|
221
|
+
/// # Fields
|
|
222
|
+
///
|
|
223
|
+
/// - `enumerator`: Boxed `StreamSource` trait object that yields chunks
|
|
224
|
+
/// - `status`: HTTP status code (typically 200)
|
|
225
|
+
/// - `headers`: Response headers (e.g., Content-Type, Cache-Control)
|
|
226
|
+
Streaming {
|
|
227
|
+
/// Stream of data chunks
|
|
228
|
+
enumerator: Box<dyn StreamSource>,
|
|
229
|
+
/// HTTP status code
|
|
230
|
+
status: u16,
|
|
231
|
+
/// Response headers
|
|
232
|
+
headers: HashMap<String, String>,
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
/// Custom response with explicit control over HTTP semantics
|
|
236
|
+
///
|
|
237
|
+
/// Used when the handler wants fine-grained control:
|
|
238
|
+
/// - Custom status codes (201, 202, 204, 3xx, 4xx, 5xx)
|
|
239
|
+
/// - Custom response headers
|
|
240
|
+
/// - Pre-encoded binary body
|
|
241
|
+
/// - Streaming with custom status/headers
|
|
242
|
+
///
|
|
243
|
+
/// # Fields
|
|
244
|
+
///
|
|
245
|
+
/// - `status`: HTTP status code
|
|
246
|
+
/// - `headers`: Custom headers
|
|
247
|
+
/// - `body`: JSON body (None for 204 No Content or streaming responses)
|
|
248
|
+
/// - `raw_body`: Pre-encoded bytes (takes precedence over `body` if present)
|
|
249
|
+
Custom {
|
|
250
|
+
/// HTTP status code
|
|
251
|
+
status: u16,
|
|
252
|
+
/// Response headers
|
|
253
|
+
headers: HashMap<String, String>,
|
|
254
|
+
/// JSON response body (may be None)
|
|
255
|
+
body: Option<Value>,
|
|
256
|
+
/// Pre-encoded response body (takes precedence over body)
|
|
257
|
+
raw_body: Option<Vec<u8>>,
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/// Plain JSON response with default HTTP semantics
|
|
261
|
+
///
|
|
262
|
+
/// Simplest and most common response type. Automatically:
|
|
263
|
+
/// - Sets status to 200 OK
|
|
264
|
+
/// - Sets Content-Type: application/json
|
|
265
|
+
/// - Serializes body as JSON
|
|
266
|
+
///
|
|
267
|
+
/// # Fields
|
|
268
|
+
///
|
|
269
|
+
/// - `body`: The JSON response body
|
|
270
|
+
Plain { body: Value },
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
impl std::fmt::Debug for InterpretedResponse {
|
|
274
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
275
|
+
match self {
|
|
276
|
+
Self::Streaming { status, headers, .. } => f
|
|
277
|
+
.debug_struct("Streaming")
|
|
278
|
+
.field("status", status)
|
|
279
|
+
.field("headers", headers)
|
|
280
|
+
.field("enumerator", &"<StreamSource>")
|
|
281
|
+
.finish(),
|
|
282
|
+
Self::Custom {
|
|
283
|
+
status,
|
|
284
|
+
headers,
|
|
285
|
+
body,
|
|
286
|
+
raw_body,
|
|
287
|
+
} => f
|
|
288
|
+
.debug_struct("Custom")
|
|
289
|
+
.field("status", status)
|
|
290
|
+
.field("headers", headers)
|
|
291
|
+
.field("body", body)
|
|
292
|
+
.field("raw_body", &raw_body.as_ref().map(|_| "<Vec<u8>>"))
|
|
293
|
+
.finish(),
|
|
294
|
+
Self::Plain { body } => f.debug_struct("Plain").field("body", body).finish(),
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
impl InterpretedResponse {
|
|
300
|
+
/// Get the HTTP status code for this response
|
|
301
|
+
///
|
|
302
|
+
/// # Returns
|
|
303
|
+
///
|
|
304
|
+
/// The status code (200 for Plain, otherwise as specified)
|
|
305
|
+
#[must_use]
|
|
306
|
+
pub const fn status(&self) -> u16 {
|
|
307
|
+
match self {
|
|
308
|
+
Self::Streaming { status, .. } | Self::Custom { status, .. } => *status,
|
|
309
|
+
Self::Plain { .. } => 200,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// Get the response headers, if any
|
|
314
|
+
///
|
|
315
|
+
/// # Returns
|
|
316
|
+
///
|
|
317
|
+
/// - `Some(headers)` for Streaming or Custom responses with headers
|
|
318
|
+
/// - `None` for responses without custom headers
|
|
319
|
+
#[must_use]
|
|
320
|
+
#[allow(clippy::missing_const_for_fn)]
|
|
321
|
+
pub fn headers(&self) -> Option<&HashMap<String, String>> {
|
|
322
|
+
match self {
|
|
323
|
+
Self::Streaming { headers, .. } | Self::Custom { headers, .. } => Some(headers),
|
|
324
|
+
Self::Plain { .. } => None,
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/// Check if this is a streaming response
|
|
329
|
+
#[must_use]
|
|
330
|
+
pub const fn is_streaming(&self) -> bool {
|
|
331
|
+
matches!(self, Self::Streaming { .. })
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Check if this is a custom response
|
|
335
|
+
#[must_use]
|
|
336
|
+
pub const fn is_custom(&self) -> bool {
|
|
337
|
+
matches!(self, Self::Custom { .. })
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// Check if this is a plain response
|
|
341
|
+
#[must_use]
|
|
342
|
+
pub const fn is_plain(&self) -> bool {
|
|
343
|
+
matches!(self, Self::Plain { .. })
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// A trait for interpreting language-specific response values
|
|
348
|
+
///
|
|
349
|
+
/// Language bindings implement this trait to translate handler response values
|
|
350
|
+
/// into a unified [`InterpretedResponse`] enum. This consolidates response detection
|
|
351
|
+
/// logic that was previously duplicated across bindings.
|
|
352
|
+
///
|
|
353
|
+
/// # Design
|
|
354
|
+
///
|
|
355
|
+
/// Each binding has its own response patterns:
|
|
356
|
+
/// - **Python**: Functions return values, generators for streaming, Response objects for custom
|
|
357
|
+
/// - **Node.js**: Functions return values or Promises, `AsyncIterable`s for streaming, Response objects
|
|
358
|
+
/// - **Ruby**: Methods return values, Enumerables for streaming, Response objects
|
|
359
|
+
/// - **PHP**: Functions return values, Generators for streaming, Response objects
|
|
360
|
+
/// - **WASM**: Functions return values or Promises (no streaming in initial implementation)
|
|
361
|
+
///
|
|
362
|
+
/// This trait allows each binding to detect these patterns independently while
|
|
363
|
+
/// sharing the response handling downstream.
|
|
364
|
+
///
|
|
365
|
+
/// # Associated Types
|
|
366
|
+
///
|
|
367
|
+
/// - `LanguageValue`: The language's value type (`PyObject`, `JsValue`, `VALUE`, etc.)
|
|
368
|
+
/// - `Error`: The language's error type (`PyErr`, `napi::Error`, `magnus::Error`, etc.)
|
|
369
|
+
///
|
|
370
|
+
/// # Methods
|
|
371
|
+
///
|
|
372
|
+
/// Bindings should implement detection logic in `is_streaming` and `is_custom_response`,
|
|
373
|
+
/// then use those in `interpret` to construct the appropriate [`InterpretedResponse`] variant.
|
|
374
|
+
///
|
|
375
|
+
/// # Example Implementation
|
|
376
|
+
///
|
|
377
|
+
/// ```ignore
|
|
378
|
+
/// struct PythonInterpreter;
|
|
379
|
+
///
|
|
380
|
+
/// impl ResponseInterpreter for PythonInterpreter {
|
|
381
|
+
/// type LanguageValue = PyObject;
|
|
382
|
+
/// type Error = PyErr;
|
|
383
|
+
///
|
|
384
|
+
/// fn is_streaming(&self, value: &PyObject) -> bool {
|
|
385
|
+
/// // Check __iter__ and __next__ methods
|
|
386
|
+
/// Python::with_gil(|py| {
|
|
387
|
+
/// value.getattr(py, "__iter__").is_ok() &&
|
|
388
|
+
/// value.getattr(py, "__next__").is_ok() &&
|
|
389
|
+
/// !is_dict(value)
|
|
390
|
+
/// })
|
|
391
|
+
/// }
|
|
392
|
+
///
|
|
393
|
+
/// fn is_custom_response(&self, value: &PyObject) -> bool {
|
|
394
|
+
/// // Check for status_code or headers attributes
|
|
395
|
+
/// Python::with_gil(|py| {
|
|
396
|
+
/// value.getattr(py, "status_code").is_ok() ||
|
|
397
|
+
/// value.getattr(py, "headers").is_ok()
|
|
398
|
+
/// })
|
|
399
|
+
/// }
|
|
400
|
+
///
|
|
401
|
+
/// fn interpret(&self, value: &PyObject) -> Result<InterpretedResponse, PyErr> {
|
|
402
|
+
/// if self.is_streaming(value) {
|
|
403
|
+
/// // Create StreamSource wrapper
|
|
404
|
+
/// } else if self.is_custom_response(value) {
|
|
405
|
+
/// // Extract status, headers, body
|
|
406
|
+
/// } else {
|
|
407
|
+
/// // Treat as plain JSON
|
|
408
|
+
/// }
|
|
409
|
+
/// }
|
|
410
|
+
/// }
|
|
411
|
+
/// ```
|
|
412
|
+
pub trait ResponseInterpreter {
|
|
413
|
+
/// The language-specific response value type
|
|
414
|
+
///
|
|
415
|
+
/// Examples: `PyObject` (Python), `JsValue` (Node.js), `VALUE` (Ruby), etc.
|
|
416
|
+
type LanguageValue;
|
|
417
|
+
|
|
418
|
+
/// The language-specific error type
|
|
419
|
+
///
|
|
420
|
+
/// Examples: `PyErr` (Python), `napi::Error` (Node.js), `magnus::Error` (Ruby), etc.
|
|
421
|
+
type Error: std::fmt::Display;
|
|
422
|
+
|
|
423
|
+
/// Check if a value is a streaming response
|
|
424
|
+
///
|
|
425
|
+
/// Streaming responses are iterables that yield chunks incrementally.
|
|
426
|
+
/// This method should detect the language-specific pattern for iteration
|
|
427
|
+
/// without consuming the value.
|
|
428
|
+
///
|
|
429
|
+
/// # Implementation Notes
|
|
430
|
+
///
|
|
431
|
+
/// - Should not consume the value (non-mutable, non-destructive check)
|
|
432
|
+
/// - Should return false for dicts/objects (they're custom responses or plain values)
|
|
433
|
+
/// - Should return true for iterators, generators, async iterables, etc.
|
|
434
|
+
///
|
|
435
|
+
/// # Examples
|
|
436
|
+
///
|
|
437
|
+
/// - Python: `hasattr(obj, '__iter__') and hasattr(obj, '__next__') and not isinstance(obj, dict)`
|
|
438
|
+
/// - Node.js: `obj && typeof obj[Symbol.asyncIterator] === 'function'`
|
|
439
|
+
/// - Ruby: `obj.respond_to?(:each) and !obj.is_a?(Hash)`
|
|
440
|
+
///
|
|
441
|
+
/// # Returns
|
|
442
|
+
///
|
|
443
|
+
/// `true` if the value is a streaming response, `false` otherwise
|
|
444
|
+
fn is_streaming(&self, value: &Self::LanguageValue) -> bool;
|
|
445
|
+
|
|
446
|
+
/// Check if a value is a custom response object
|
|
447
|
+
///
|
|
448
|
+
/// Custom responses have explicit status codes, headers, or body control.
|
|
449
|
+
/// This method should detect the language-specific pattern for response objects
|
|
450
|
+
/// without consuming the value.
|
|
451
|
+
///
|
|
452
|
+
/// # Implementation Notes
|
|
453
|
+
///
|
|
454
|
+
/// - Should not consume the value (non-mutable, non-destructive check)
|
|
455
|
+
/// - Should return false for plain JSON types (primitives, arrays, plain dicts)
|
|
456
|
+
/// - Should return true for response wrapper objects
|
|
457
|
+
///
|
|
458
|
+
/// # Examples
|
|
459
|
+
///
|
|
460
|
+
/// - Python: `hasattr(obj, 'status_code') or hasattr(obj, 'headers')`
|
|
461
|
+
/// - Node.js: `obj && (obj.statusCode !== undefined or obj.status !== undefined)`
|
|
462
|
+
/// - Ruby: `obj.respond_to?(:status_code) or obj.respond_to?(:headers)`
|
|
463
|
+
///
|
|
464
|
+
/// # Returns
|
|
465
|
+
///
|
|
466
|
+
/// `true` if the value is a custom response, `false` otherwise
|
|
467
|
+
fn is_custom_response(&self, value: &Self::LanguageValue) -> bool;
|
|
468
|
+
|
|
469
|
+
/// Interpret a language-specific value into a unified response format
|
|
470
|
+
///
|
|
471
|
+
/// This method performs the full interpretation logic:
|
|
472
|
+
/// 1. Checks if streaming (call `is_streaming()`)
|
|
473
|
+
/// 2. Checks if custom response (call `is_custom_response()`)
|
|
474
|
+
/// 3. Otherwise treats as plain JSON
|
|
475
|
+
///
|
|
476
|
+
/// Each branch extracts the relevant data and constructs an [`InterpretedResponse`].
|
|
477
|
+
///
|
|
478
|
+
/// # Arguments
|
|
479
|
+
///
|
|
480
|
+
/// * `value` - The language-specific value from the handler
|
|
481
|
+
///
|
|
482
|
+
/// # Returns
|
|
483
|
+
///
|
|
484
|
+
/// - `Ok(InterpretedResponse)` - Successfully interpreted response
|
|
485
|
+
/// - `Err(Self::Error)` - Error during interpretation (type conversion, missing fields, etc.)
|
|
486
|
+
///
|
|
487
|
+
/// # Errors
|
|
488
|
+
///
|
|
489
|
+
/// Returns `Err(Self::Error)` if:
|
|
490
|
+
/// - Streaming source creation failed
|
|
491
|
+
/// - Status code extraction failed
|
|
492
|
+
/// - Header extraction/conversion failed
|
|
493
|
+
/// - Body conversion to JSON failed
|
|
494
|
+
fn interpret(&self, value: &Self::LanguageValue) -> Result<InterpretedResponse, Self::Error>;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
#[cfg(test)]
|
|
498
|
+
mod tests {
|
|
499
|
+
use super::*;
|
|
500
|
+
|
|
501
|
+
/// Mock `StreamSource` for testing
|
|
502
|
+
struct MockStreamSource {
|
|
503
|
+
chunks: Vec<Vec<u8>>,
|
|
504
|
+
index: usize,
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
impl MockStreamSource {
|
|
508
|
+
fn new(chunks: Vec<Vec<u8>>) -> Self {
|
|
509
|
+
Self { chunks, index: 0 }
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
impl StreamSource for MockStreamSource {
|
|
514
|
+
fn next_chunk(&mut self) -> Option<Vec<u8>> {
|
|
515
|
+
if self.index < self.chunks.len() {
|
|
516
|
+
let chunk = self.chunks[self.index].clone();
|
|
517
|
+
self.index += 1;
|
|
518
|
+
Some(chunk)
|
|
519
|
+
} else {
|
|
520
|
+
None
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/// Mock interpreter for testing
|
|
526
|
+
enum TestValue {
|
|
527
|
+
Stream,
|
|
528
|
+
Custom,
|
|
529
|
+
Plain,
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#[derive(Debug)]
|
|
533
|
+
struct TestError(String);
|
|
534
|
+
|
|
535
|
+
impl std::fmt::Display for TestError {
|
|
536
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
537
|
+
write!(f, "{}", self.0)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
struct TestInterpreter;
|
|
542
|
+
|
|
543
|
+
impl ResponseInterpreter for TestInterpreter {
|
|
544
|
+
type LanguageValue = TestValue;
|
|
545
|
+
type Error = TestError;
|
|
546
|
+
|
|
547
|
+
fn is_streaming(&self, value: &Self::LanguageValue) -> bool {
|
|
548
|
+
matches!(value, TestValue::Stream)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
fn is_custom_response(&self, value: &Self::LanguageValue) -> bool {
|
|
552
|
+
matches!(value, TestValue::Custom)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
fn interpret(&self, value: &Self::LanguageValue) -> Result<InterpretedResponse, Self::Error> {
|
|
556
|
+
match value {
|
|
557
|
+
TestValue::Stream => Ok(InterpretedResponse::Streaming {
|
|
558
|
+
enumerator: Box::new(MockStreamSource::new(vec![b"chunk1".to_vec(), b"chunk2".to_vec()])),
|
|
559
|
+
status: 200,
|
|
560
|
+
headers: HashMap::new(),
|
|
561
|
+
}),
|
|
562
|
+
TestValue::Custom => {
|
|
563
|
+
let mut headers = HashMap::new();
|
|
564
|
+
headers.insert("x-custom".to_string(), "header".to_string());
|
|
565
|
+
Ok(InterpretedResponse::Custom {
|
|
566
|
+
status: 201,
|
|
567
|
+
headers,
|
|
568
|
+
body: Some(Value::from("custom body")),
|
|
569
|
+
raw_body: None,
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
TestValue::Plain => Ok(InterpretedResponse::Plain {
|
|
573
|
+
body: Value::from("plain body"),
|
|
574
|
+
}),
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
#[test]
|
|
580
|
+
fn test_stream_source_trait_object_safety() {
|
|
581
|
+
let mut stream = MockStreamSource::new(vec![b"test".to_vec()]);
|
|
582
|
+
assert_eq!(stream.next_chunk(), Some(b"test".to_vec()));
|
|
583
|
+
assert_eq!(stream.next_chunk(), None);
|
|
584
|
+
|
|
585
|
+
// Verify we can box it
|
|
586
|
+
let mut boxed: Box<dyn StreamSource> = Box::new(MockStreamSource::new(vec![b"test".to_vec()]));
|
|
587
|
+
assert!(boxed.next_chunk().is_some());
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
#[test]
|
|
591
|
+
fn test_interpreted_response_streaming_construction() {
|
|
592
|
+
let stream = MockStreamSource::new(vec![b"chunk".to_vec()]);
|
|
593
|
+
let mut headers = HashMap::new();
|
|
594
|
+
headers.insert("content-type".to_string(), "application/octet-stream".to_string());
|
|
595
|
+
|
|
596
|
+
let response = InterpretedResponse::Streaming {
|
|
597
|
+
enumerator: Box::new(stream),
|
|
598
|
+
status: 200,
|
|
599
|
+
headers,
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
assert!(response.is_streaming());
|
|
603
|
+
assert!(!response.is_custom());
|
|
604
|
+
assert!(!response.is_plain());
|
|
605
|
+
assert_eq!(response.status(), 200);
|
|
606
|
+
assert!(response.headers().is_some());
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#[test]
|
|
610
|
+
fn test_interpreted_response_custom_construction() {
|
|
611
|
+
let mut headers = HashMap::new();
|
|
612
|
+
headers.insert("x-custom-header".to_string(), "value".to_string());
|
|
613
|
+
|
|
614
|
+
let response = InterpretedResponse::Custom {
|
|
615
|
+
status: 201,
|
|
616
|
+
headers,
|
|
617
|
+
body: Some(serde_json::json!({ "id": 42 })),
|
|
618
|
+
raw_body: None,
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
assert!(!response.is_streaming());
|
|
622
|
+
assert!(response.is_custom());
|
|
623
|
+
assert!(!response.is_plain());
|
|
624
|
+
assert_eq!(response.status(), 201);
|
|
625
|
+
assert!(response.headers().is_some());
|
|
626
|
+
assert_eq!(response.headers().unwrap().len(), 1);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
#[test]
|
|
630
|
+
fn test_interpreted_response_plain_construction() {
|
|
631
|
+
let response = InterpretedResponse::Plain {
|
|
632
|
+
body: serde_json::json!({ "message": "hello" }),
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
assert!(!response.is_streaming());
|
|
636
|
+
assert!(!response.is_custom());
|
|
637
|
+
assert!(response.is_plain());
|
|
638
|
+
assert_eq!(response.status(), 200);
|
|
639
|
+
assert_eq!(response.headers(), None);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
#[test]
|
|
643
|
+
fn test_interpreted_response_status_codes() {
|
|
644
|
+
let codes = vec![200u16, 201, 202, 204, 400, 401, 403, 404, 500, 502, 503];
|
|
645
|
+
|
|
646
|
+
for code in codes {
|
|
647
|
+
let response = InterpretedResponse::Custom {
|
|
648
|
+
status: code,
|
|
649
|
+
headers: HashMap::new(),
|
|
650
|
+
body: None,
|
|
651
|
+
raw_body: None,
|
|
652
|
+
};
|
|
653
|
+
assert_eq!(response.status(), code);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#[test]
|
|
658
|
+
fn test_interpreted_response_headers_empty() {
|
|
659
|
+
let response = InterpretedResponse::Custom {
|
|
660
|
+
status: 200,
|
|
661
|
+
headers: HashMap::new(),
|
|
662
|
+
body: None,
|
|
663
|
+
raw_body: None,
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
let headers = response.headers().unwrap();
|
|
667
|
+
assert_eq!(headers.len(), 0);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
#[test]
|
|
671
|
+
fn test_interpreted_response_headers_multiple() {
|
|
672
|
+
let mut headers = HashMap::new();
|
|
673
|
+
headers.insert("content-type".to_string(), "application/json".to_string());
|
|
674
|
+
headers.insert("cache-control".to_string(), "no-cache".to_string());
|
|
675
|
+
headers.insert("x-custom".to_string(), "value".to_string());
|
|
676
|
+
|
|
677
|
+
let response = InterpretedResponse::Custom {
|
|
678
|
+
status: 200,
|
|
679
|
+
headers,
|
|
680
|
+
body: None,
|
|
681
|
+
raw_body: None,
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
let resp_headers = response.headers().unwrap();
|
|
685
|
+
assert_eq!(resp_headers.len(), 3);
|
|
686
|
+
assert_eq!(resp_headers.get("content-type"), Some(&"application/json".to_string()));
|
|
687
|
+
assert_eq!(resp_headers.get("cache-control"), Some(&"no-cache".to_string()));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
#[test]
|
|
691
|
+
fn test_interpreted_response_body_json() {
|
|
692
|
+
let body = serde_json::json!({
|
|
693
|
+
"id": 42,
|
|
694
|
+
"name": "test",
|
|
695
|
+
"nested": {
|
|
696
|
+
"value": true
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
let response = InterpretedResponse::Plain { body };
|
|
701
|
+
|
|
702
|
+
// Verify response preserves the value
|
|
703
|
+
match response {
|
|
704
|
+
InterpretedResponse::Plain {
|
|
705
|
+
body: ref returned_body,
|
|
706
|
+
} => {
|
|
707
|
+
assert_eq!(returned_body["id"], 42);
|
|
708
|
+
assert_eq!(returned_body["name"], "test");
|
|
709
|
+
assert_eq!(returned_body["nested"]["value"], true);
|
|
710
|
+
}
|
|
711
|
+
_ => panic!("Expected Plain response"),
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
#[test]
|
|
716
|
+
fn test_interpreted_response_raw_body_precedence() {
|
|
717
|
+
let response = InterpretedResponse::Custom {
|
|
718
|
+
status: 200,
|
|
719
|
+
headers: HashMap::new(),
|
|
720
|
+
body: Some(serde_json::json!({ "ignored": true })),
|
|
721
|
+
raw_body: Some(b"raw bytes".to_vec()),
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
match response {
|
|
725
|
+
InterpretedResponse::Custom {
|
|
726
|
+
body,
|
|
727
|
+
raw_body: Some(raw),
|
|
728
|
+
..
|
|
729
|
+
} => {
|
|
730
|
+
assert!(body.is_some()); // body is still present
|
|
731
|
+
assert_eq!(raw, b"raw bytes");
|
|
732
|
+
}
|
|
733
|
+
_ => panic!("Expected Custom response with raw_body"),
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
#[test]
|
|
738
|
+
fn test_response_interpreter_streaming() {
|
|
739
|
+
let interpreter = TestInterpreter;
|
|
740
|
+
let result = interpreter.interpret(&TestValue::Stream).unwrap();
|
|
741
|
+
|
|
742
|
+
assert!(result.is_streaming());
|
|
743
|
+
assert_eq!(result.status(), 200);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
#[test]
|
|
747
|
+
fn test_response_interpreter_custom() {
|
|
748
|
+
let interpreter = TestInterpreter;
|
|
749
|
+
let result = interpreter.interpret(&TestValue::Custom).unwrap();
|
|
750
|
+
|
|
751
|
+
assert!(result.is_custom());
|
|
752
|
+
assert_eq!(result.status(), 201);
|
|
753
|
+
assert_eq!(result.headers().unwrap().get("x-custom"), Some(&"header".to_string()));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
#[test]
|
|
757
|
+
fn test_response_interpreter_plain() {
|
|
758
|
+
let interpreter = TestInterpreter;
|
|
759
|
+
let result = interpreter.interpret(&TestValue::Plain).unwrap();
|
|
760
|
+
|
|
761
|
+
assert!(result.is_plain());
|
|
762
|
+
assert_eq!(result.status(), 200);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
#[test]
|
|
766
|
+
fn test_stream_source_multiple_chunks() {
|
|
767
|
+
let chunks = vec![b"first".to_vec(), b"second".to_vec(), b"third".to_vec()];
|
|
768
|
+
let mut stream = MockStreamSource::new(chunks);
|
|
769
|
+
|
|
770
|
+
assert_eq!(stream.next_chunk(), Some(b"first".to_vec()));
|
|
771
|
+
assert_eq!(stream.next_chunk(), Some(b"second".to_vec()));
|
|
772
|
+
assert_eq!(stream.next_chunk(), Some(b"third".to_vec()));
|
|
773
|
+
assert_eq!(stream.next_chunk(), None);
|
|
774
|
+
assert_eq!(stream.next_chunk(), None); // Idempotent
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
#[test]
|
|
778
|
+
fn test_stream_source_empty() {
|
|
779
|
+
let mut stream = MockStreamSource::new(vec![]);
|
|
780
|
+
assert_eq!(stream.next_chunk(), None);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
#[test]
|
|
784
|
+
fn test_stream_source_large_chunks() {
|
|
785
|
+
let large_chunk = vec![0u8; 1024 * 1024]; // 1MB
|
|
786
|
+
let mut stream = MockStreamSource::new(vec![large_chunk]);
|
|
787
|
+
|
|
788
|
+
let retrieved = stream.next_chunk().unwrap();
|
|
789
|
+
assert_eq!(retrieved.len(), 1024 * 1024);
|
|
790
|
+
assert_eq!(stream.next_chunk(), None);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
#[test]
|
|
794
|
+
fn test_streaming_response_headers_empty() {
|
|
795
|
+
let response = InterpretedResponse::Streaming {
|
|
796
|
+
enumerator: Box::new(MockStreamSource::new(vec![])),
|
|
797
|
+
status: 200,
|
|
798
|
+
headers: HashMap::new(),
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
let headers = response.headers().unwrap();
|
|
802
|
+
assert!(headers.is_empty());
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
#[test]
|
|
806
|
+
fn test_streaming_response_headers_with_values() {
|
|
807
|
+
let mut headers = HashMap::new();
|
|
808
|
+
headers.insert("transfer-encoding".to_string(), "chunked".to_string());
|
|
809
|
+
headers.insert("content-type".to_string(), "application/json".to_string());
|
|
810
|
+
|
|
811
|
+
let response = InterpretedResponse::Streaming {
|
|
812
|
+
enumerator: Box::new(MockStreamSource::new(vec![])),
|
|
813
|
+
status: 200,
|
|
814
|
+
headers,
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
let resp_headers = response.headers().unwrap();
|
|
818
|
+
assert_eq!(resp_headers.len(), 2);
|
|
819
|
+
assert_eq!(resp_headers.get("transfer-encoding"), Some(&"chunked".to_string()));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
#[test]
|
|
823
|
+
fn test_custom_response_no_body() {
|
|
824
|
+
let response = InterpretedResponse::Custom {
|
|
825
|
+
status: 204,
|
|
826
|
+
headers: HashMap::new(),
|
|
827
|
+
body: None,
|
|
828
|
+
raw_body: None,
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
match response {
|
|
832
|
+
InterpretedResponse::Custom { body: None, .. } => {
|
|
833
|
+
// Expected: 204 No Content
|
|
834
|
+
}
|
|
835
|
+
_ => panic!("Expected Custom response with no body"),
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
#[test]
|
|
840
|
+
fn test_custom_response_json_body() {
|
|
841
|
+
let body = serde_json::json!({
|
|
842
|
+
"success": true,
|
|
843
|
+
"data": [1, 2, 3]
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
let response = InterpretedResponse::Custom {
|
|
847
|
+
status: 201,
|
|
848
|
+
headers: HashMap::new(),
|
|
849
|
+
body: Some(body),
|
|
850
|
+
raw_body: None,
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
match response {
|
|
854
|
+
InterpretedResponse::Custom {
|
|
855
|
+
status: 201,
|
|
856
|
+
body: Some(b),
|
|
857
|
+
..
|
|
858
|
+
} => {
|
|
859
|
+
assert_eq!(b["success"], true);
|
|
860
|
+
assert_eq!(b["data"][0], 1);
|
|
861
|
+
}
|
|
862
|
+
_ => panic!("Expected Custom response with JSON body"),
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
#[test]
|
|
867
|
+
fn test_plain_response_various_json_types() {
|
|
868
|
+
// Test with object
|
|
869
|
+
let obj = InterpretedResponse::Plain {
|
|
870
|
+
body: serde_json::json!({ "key": "value" }),
|
|
871
|
+
};
|
|
872
|
+
assert!(obj.is_plain());
|
|
873
|
+
|
|
874
|
+
// Test with array
|
|
875
|
+
let arr = InterpretedResponse::Plain {
|
|
876
|
+
body: serde_json::json!([1, 2, 3]),
|
|
877
|
+
};
|
|
878
|
+
assert!(arr.is_plain());
|
|
879
|
+
|
|
880
|
+
// Test with string
|
|
881
|
+
let str_val = InterpretedResponse::Plain {
|
|
882
|
+
body: serde_json::json!("hello"),
|
|
883
|
+
};
|
|
884
|
+
assert!(str_val.is_plain());
|
|
885
|
+
|
|
886
|
+
// Test with number
|
|
887
|
+
let num = InterpretedResponse::Plain {
|
|
888
|
+
body: serde_json::json!(42),
|
|
889
|
+
};
|
|
890
|
+
assert!(num.is_plain());
|
|
891
|
+
|
|
892
|
+
// Test with null
|
|
893
|
+
let null_val = InterpretedResponse::Plain {
|
|
894
|
+
body: serde_json::Value::Null,
|
|
895
|
+
};
|
|
896
|
+
assert!(null_val.is_plain());
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
#[test]
|
|
900
|
+
fn test_stream_source_send_sync() {
|
|
901
|
+
// Verify that StreamSource trait objects are Send + Sync
|
|
902
|
+
let stream = MockStreamSource::new(vec![b"test".to_vec()]);
|
|
903
|
+
let boxed: Box<dyn StreamSource> = Box::new(stream);
|
|
904
|
+
|
|
905
|
+
// This compiles only if Box<dyn StreamSource> implements Send + Sync
|
|
906
|
+
let _: Box<dyn StreamSource + Send + Sync> = boxed;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
#[test]
|
|
910
|
+
fn test_interpreted_response_debug() {
|
|
911
|
+
let plain = InterpretedResponse::Plain {
|
|
912
|
+
body: serde_json::json!({ "test": true }),
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
let debug_string = format!("{plain:?}");
|
|
916
|
+
assert!(debug_string.contains("Plain"));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
#[test]
|
|
920
|
+
fn test_custom_response_with_all_fields() {
|
|
921
|
+
let mut headers = HashMap::new();
|
|
922
|
+
headers.insert("content-type".to_string(), "application/json".to_string());
|
|
923
|
+
headers.insert("x-custom".to_string(), "header-value".to_string());
|
|
924
|
+
|
|
925
|
+
let response = InterpretedResponse::Custom {
|
|
926
|
+
status: 200,
|
|
927
|
+
headers,
|
|
928
|
+
body: Some(serde_json::json!({ "data": "value" })),
|
|
929
|
+
raw_body: Some(b"fallback".to_vec()),
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
match response {
|
|
933
|
+
InterpretedResponse::Custom {
|
|
934
|
+
status: 200,
|
|
935
|
+
headers: h,
|
|
936
|
+
body: Some(_),
|
|
937
|
+
raw_body: Some(_),
|
|
938
|
+
} => {
|
|
939
|
+
assert_eq!(h.len(), 2);
|
|
940
|
+
}
|
|
941
|
+
_ => panic!("Expected Custom response with all fields"),
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|