spikard 0.12.0 → 0.15.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.
Files changed (206) hide show
  1. checksums.yaml +4 -4
  2. data/Steepfile +6 -0
  3. data/ext/spikard_rb/extconf.rb +1 -2
  4. data/ext/spikard_rb/{Cargo.lock → native/Cargo.lock} +897 -451
  5. data/ext/spikard_rb/native/Cargo.toml +24 -0
  6. data/ext/spikard_rb/src/lib.rs +5366 -3
  7. data/lib/spikard/native.rb +86 -0
  8. data/lib/spikard/version.rb +6 -1
  9. data/lib/spikard.rb +8 -45
  10. data/lib/spikard_rb.so +0 -0
  11. data/sig/types.rbs +427 -0
  12. metadata +14 -242
  13. data/LICENSE +0 -1
  14. data/README.md +0 -267
  15. data/ext/spikard_rb/Cargo.toml +0 -17
  16. data/lib/spikard/app.rb +0 -428
  17. data/lib/spikard/background.rb +0 -58
  18. data/lib/spikard/config.rb +0 -506
  19. data/lib/spikard/converters.rb +0 -13
  20. data/lib/spikard/grpc.rb +0 -182
  21. data/lib/spikard/handler_wrapper.rb +0 -113
  22. data/lib/spikard/provide.rb +0 -214
  23. data/lib/spikard/response.rb +0 -173
  24. data/lib/spikard/schema.rb +0 -243
  25. data/lib/spikard/sse.rb +0 -111
  26. data/lib/spikard/streaming_response.rb +0 -44
  27. data/lib/spikard/testing.rb +0 -432
  28. data/lib/spikard/upload_file.rb +0 -131
  29. data/lib/spikard/websocket.rb +0 -59
  30. data/sig/spikard.rbs +0 -719
  31. data/vendor/crates/spikard-bindings-shared/Cargo.toml +0 -80
  32. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +0 -132
  33. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +0 -905
  34. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +0 -210
  35. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +0 -252
  36. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +0 -404
  37. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +0 -199
  38. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +0 -252
  39. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +0 -829
  40. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +0 -587
  41. data/vendor/crates/spikard-bindings-shared/src/lib.rs +0 -33
  42. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +0 -298
  43. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +0 -594
  44. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +0 -743
  45. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +0 -944
  46. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +0 -260
  47. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +0 -369
  48. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +0 -192
  49. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +0 -383
  50. data/vendor/crates/spikard-bindings-shared/tests/full_coverage.rs +0 -459
  51. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +0 -280
  52. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +0 -669
  53. data/vendor/crates/spikard-core/Cargo.toml +0 -60
  54. data/vendor/crates/spikard-core/src/bindings/mod.rs +0 -3
  55. data/vendor/crates/spikard-core/src/bindings/response.rs +0 -130
  56. data/vendor/crates/spikard-core/src/debug.rs +0 -127
  57. data/vendor/crates/spikard-core/src/di/container.rs +0 -702
  58. data/vendor/crates/spikard-core/src/di/dependency.rs +0 -273
  59. data/vendor/crates/spikard-core/src/di/error.rs +0 -118
  60. data/vendor/crates/spikard-core/src/di/factory.rs +0 -538
  61. data/vendor/crates/spikard-core/src/di/graph.rs +0 -507
  62. data/vendor/crates/spikard-core/src/di/mod.rs +0 -192
  63. data/vendor/crates/spikard-core/src/di/resolved.rs +0 -428
  64. data/vendor/crates/spikard-core/src/di/value.rs +0 -282
  65. data/vendor/crates/spikard-core/src/errors.rs +0 -72
  66. data/vendor/crates/spikard-core/src/http.rs +0 -492
  67. data/vendor/crates/spikard-core/src/lib.rs +0 -29
  68. data/vendor/crates/spikard-core/src/lifecycle.rs +0 -1273
  69. data/vendor/crates/spikard-core/src/metadata.rs +0 -378
  70. data/vendor/crates/spikard-core/src/parameters.rs +0 -2546
  71. data/vendor/crates/spikard-core/src/problem.rs +0 -358
  72. data/vendor/crates/spikard-core/src/request_data.rs +0 -1146
  73. data/vendor/crates/spikard-core/src/router.rs +0 -530
  74. data/vendor/crates/spikard-core/src/schema_registry.rs +0 -197
  75. data/vendor/crates/spikard-core/src/type_hints.rs +0 -311
  76. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +0 -710
  77. data/vendor/crates/spikard-core/src/validation/mod.rs +0 -470
  78. data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +0 -136
  79. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +0 -37
  80. data/vendor/crates/spikard-core/tests/error_mapper.rs +0 -761
  81. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +0 -106
  82. data/vendor/crates/spikard-core/tests/parameters_full.rs +0 -701
  83. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +0 -301
  84. data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +0 -67
  85. data/vendor/crates/spikard-core/tests/validation_coverage.rs +0 -250
  86. data/vendor/crates/spikard-core/tests/validation_error_paths.rs +0 -45
  87. data/vendor/crates/spikard-http/Cargo.toml +0 -87
  88. data/vendor/crates/spikard-http/examples/sse-notifications.rs +0 -148
  89. data/vendor/crates/spikard-http/examples/websocket-chat.rs +0 -92
  90. data/vendor/crates/spikard-http/src/auth.rs +0 -301
  91. data/vendor/crates/spikard-http/src/background.rs +0 -1860
  92. data/vendor/crates/spikard-http/src/bindings/mod.rs +0 -3
  93. data/vendor/crates/spikard-http/src/bindings/response.rs +0 -1
  94. data/vendor/crates/spikard-http/src/body_metadata.rs +0 -8
  95. data/vendor/crates/spikard-http/src/cors.rs +0 -1026
  96. data/vendor/crates/spikard-http/src/debug.rs +0 -128
  97. data/vendor/crates/spikard-http/src/di_handler.rs +0 -1672
  98. data/vendor/crates/spikard-http/src/grpc/framing.rs +0 -469
  99. data/vendor/crates/spikard-http/src/grpc/handler.rs +0 -1122
  100. data/vendor/crates/spikard-http/src/grpc/mod.rs +0 -434
  101. data/vendor/crates/spikard-http/src/grpc/service.rs +0 -622
  102. data/vendor/crates/spikard-http/src/grpc/streaming.rs +0 -319
  103. data/vendor/crates/spikard-http/src/handler_response.rs +0 -901
  104. data/vendor/crates/spikard-http/src/handler_trait.rs +0 -1015
  105. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +0 -290
  106. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +0 -502
  107. data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +0 -648
  108. data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +0 -58
  109. data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +0 -1207
  110. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +0 -2262
  111. data/vendor/crates/spikard-http/src/lib.rs +0 -548
  112. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -230
  113. data/vendor/crates/spikard-http/src/lifecycle.rs +0 -1193
  114. data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -560
  115. data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -912
  116. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -513
  117. data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -768
  118. data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
  119. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -535
  120. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -1363
  121. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -667
  122. data/vendor/crates/spikard-http/src/query_parser.rs +0 -793
  123. data/vendor/crates/spikard-http/src/response.rs +0 -720
  124. data/vendor/crates/spikard-http/src/server/fast_router.rs +0 -186
  125. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +0 -858
  126. data/vendor/crates/spikard-http/src/server/handler.rs +0 -1661
  127. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -253
  128. data/vendor/crates/spikard-http/src/server/mod.rs +0 -1649
  129. data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -871
  130. data/vendor/crates/spikard-http/src/server/routing_factory.rs +0 -618
  131. data/vendor/crates/spikard-http/src/sse.rs +0 -1409
  132. data/vendor/crates/spikard-http/src/testing/form.rs +0 -52
  133. data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -64
  134. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -787
  135. data/vendor/crates/spikard-http/src/testing.rs +0 -617
  136. data/vendor/crates/spikard-http/src/websocket.rs +0 -1477
  137. data/vendor/crates/spikard-http/tests/auth_integration.rs +0 -645
  138. data/vendor/crates/spikard-http/tests/background_behavior.rs +0 -832
  139. data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +0 -1012
  140. data/vendor/crates/spikard-http/tests/common/handlers.rs +0 -309
  141. data/vendor/crates/spikard-http/tests/common/mod.rs +0 -33
  142. data/vendor/crates/spikard-http/tests/common/test_builders.rs +0 -628
  143. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +0 -162
  144. data/vendor/crates/spikard-http/tests/di_integration.rs +0 -192
  145. data/vendor/crates/spikard-http/tests/doc_snippets.rs +0 -5
  146. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +0 -430
  147. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +0 -738
  148. data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +0 -652
  149. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +0 -334
  150. data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +0 -532
  151. data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +0 -495
  152. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +0 -974
  153. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +0 -1093
  154. data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +0 -389
  155. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +0 -656
  156. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +0 -513
  157. data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +0 -328
  158. data/vendor/crates/spikard-http/tests/server_config_builder.rs +0 -314
  159. data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +0 -200
  160. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +0 -83
  161. data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +0 -464
  162. data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +0 -286
  163. data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +0 -118
  164. data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +0 -99
  165. data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +0 -204
  166. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +0 -421
  167. data/vendor/crates/spikard-http/tests/server_router_behavior.rs +0 -121
  168. data/vendor/crates/spikard-http/tests/sse_behavior.rs +0 -620
  169. data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +0 -584
  170. data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +0 -130
  171. data/vendor/crates/spikard-http/tests/test_client_requests.rs +0 -167
  172. data/vendor/crates/spikard-http/tests/testing_helpers.rs +0 -87
  173. data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +0 -155
  174. data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +0 -82
  175. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +0 -663
  176. data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +0 -440
  177. data/vendor/crates/spikard-http/tests/websocket_integration.rs +0 -150
  178. data/vendor/crates/spikard-rb/Cargo.toml +0 -68
  179. data/vendor/crates/spikard-rb/build.rs +0 -200
  180. data/vendor/crates/spikard-rb/src/background.rs +0 -63
  181. data/vendor/crates/spikard-rb/src/config/mod.rs +0 -5
  182. data/vendor/crates/spikard-rb/src/config/server_config.rs +0 -401
  183. data/vendor/crates/spikard-rb/src/conversion.rs +0 -688
  184. data/vendor/crates/spikard-rb/src/di/builder.rs +0 -100
  185. data/vendor/crates/spikard-rb/src/di/mod.rs +0 -375
  186. data/vendor/crates/spikard-rb/src/grpc/handler.rs +0 -834
  187. data/vendor/crates/spikard-rb/src/grpc/mod.rs +0 -13
  188. data/vendor/crates/spikard-rb/src/gvl.rs +0 -80
  189. data/vendor/crates/spikard-rb/src/handler.rs +0 -699
  190. data/vendor/crates/spikard-rb/src/integration/mod.rs +0 -3
  191. data/vendor/crates/spikard-rb/src/lib.rs +0 -2264
  192. data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -303
  193. data/vendor/crates/spikard-rb/src/metadata/mod.rs +0 -5
  194. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +0 -507
  195. data/vendor/crates/spikard-rb/src/request.rs +0 -439
  196. data/vendor/crates/spikard-rb/src/runtime/mod.rs +0 -5
  197. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +0 -344
  198. data/vendor/crates/spikard-rb/src/server.rs +0 -307
  199. data/vendor/crates/spikard-rb/src/sse.rs +0 -231
  200. data/vendor/crates/spikard-rb/src/testing/client.rs +0 -698
  201. data/vendor/crates/spikard-rb/src/testing/mod.rs +0 -7
  202. data/vendor/crates/spikard-rb/src/testing/sse.rs +0 -108
  203. data/vendor/crates/spikard-rb/src/testing/websocket.rs +0 -573
  204. data/vendor/crates/spikard-rb/src/websocket.rs +0 -475
  205. data/vendor/crates/spikard-rb-macros/Cargo.toml +0 -25
  206. data/vendor/crates/spikard-rb-macros/src/lib.rs +0 -51
@@ -1,944 +0,0 @@
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
- }