spikard 0.6.2 → 0.7.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 +90 -508
- data/ext/spikard_rb/Cargo.lock +3287 -0
- data/ext/spikard_rb/Cargo.toml +1 -1
- data/ext/spikard_rb/extconf.rb +3 -3
- data/lib/spikard/app.rb +72 -49
- data/lib/spikard/background.rb +38 -7
- data/lib/spikard/testing.rb +42 -4
- data/lib/spikard/version.rb +1 -1
- data/sig/spikard.rbs +4 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +1 -1
- data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +191 -0
- data/vendor/crates/spikard-core/Cargo.toml +1 -1
- data/vendor/crates/spikard-core/src/http.rs +1 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +63 -0
- data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +136 -0
- data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +37 -0
- data/vendor/crates/spikard-core/tests/error_mapper.rs +761 -0
- data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +106 -0
- data/vendor/crates/spikard-core/tests/parameters_full.rs +701 -0
- data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +301 -0
- data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +67 -0
- data/vendor/crates/spikard-core/tests/validation_coverage.rs +250 -0
- data/vendor/crates/spikard-core/tests/validation_error_paths.rs +45 -0
- data/vendor/crates/spikard-http/Cargo.toml +1 -1
- data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +502 -0
- data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +648 -0
- data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +58 -0
- data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +1207 -0
- data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2262 -0
- data/vendor/crates/spikard-http/src/testing/test_client.rs +155 -2
- data/vendor/crates/spikard-http/src/testing.rs +171 -0
- data/vendor/crates/spikard-http/src/websocket.rs +79 -6
- data/vendor/crates/spikard-http/tests/auth_integration.rs +647 -0
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +633 -0
- data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +162 -0
- data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +389 -0
- data/vendor/crates/spikard-http/tests/request_extraction_full.rs +513 -0
- data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +244 -0
- data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +200 -0
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +82 -0
- data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +464 -0
- data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +286 -0
- data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +118 -0
- data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +99 -0
- data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +206 -0
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +281 -0
- data/vendor/crates/spikard-http/tests/server_router_behavior.rs +121 -0
- data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +584 -0
- data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +130 -0
- data/vendor/crates/spikard-http/tests/test_client_requests.rs +167 -0
- data/vendor/crates/spikard-http/tests/testing_helpers.rs +87 -0
- data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +156 -0
- data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +82 -0
- data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +440 -0
- data/vendor/crates/spikard-http/tests/websocket_integration.rs +152 -0
- data/vendor/crates/spikard-rb/Cargo.toml +1 -1
- data/vendor/crates/spikard-rb/src/gvl.rs +80 -0
- data/vendor/crates/spikard-rb/src/handler.rs +12 -9
- data/vendor/crates/spikard-rb/src/lib.rs +137 -124
- data/vendor/crates/spikard-rb/src/request.rs +342 -0
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +1 -8
- data/vendor/crates/spikard-rb/src/server.rs +1 -8
- data/vendor/crates/spikard-rb/src/testing/client.rs +168 -9
- data/vendor/crates/spikard-rb/src/websocket.rs +119 -30
- data/vendor/crates/spikard-rb-macros/Cargo.toml +14 -0
- data/vendor/crates/spikard-rb-macros/src/lib.rs +52 -0
- metadata +44 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
//! Native Request object for Ruby handlers.
|
|
2
|
+
//!
|
|
3
|
+
//! Ruby benchmarks frequently access only a subset of request fields. Building a full
|
|
4
|
+
//! Ruby Hash for every request eagerly converts headers/cookies/query/etc even when
|
|
5
|
+
//! unused. This module provides a native `Spikard::Native::Request` that lazily
|
|
6
|
+
//! materialises Ruby values on demand and caches them for subsequent access.
|
|
7
|
+
|
|
8
|
+
#![deny(clippy::unwrap_used)]
|
|
9
|
+
|
|
10
|
+
use bytes::Bytes;
|
|
11
|
+
use magnus::prelude::*;
|
|
12
|
+
use magnus::value::InnerValue;
|
|
13
|
+
use magnus::value::LazyId;
|
|
14
|
+
use magnus::value::Opaque;
|
|
15
|
+
use magnus::{Error, RHash, RString, Ruby, Symbol, Value, gc::Marker};
|
|
16
|
+
use serde_json::Value as JsonValue;
|
|
17
|
+
use spikard_http::RequestData;
|
|
18
|
+
use std::cell::RefCell;
|
|
19
|
+
use std::collections::HashMap;
|
|
20
|
+
use std::sync::Arc;
|
|
21
|
+
|
|
22
|
+
use crate::conversion::{map_to_ruby_hash, multimap_to_ruby_hash};
|
|
23
|
+
use crate::metadata::json_to_ruby;
|
|
24
|
+
|
|
25
|
+
#[derive(Default)]
|
|
26
|
+
struct RequestCache {
|
|
27
|
+
method: Option<Opaque<Value>>,
|
|
28
|
+
path: Option<Opaque<Value>>,
|
|
29
|
+
path_params: Option<Opaque<Value>>,
|
|
30
|
+
query: Option<Opaque<Value>>,
|
|
31
|
+
raw_query: Option<Opaque<Value>>,
|
|
32
|
+
headers: Option<Opaque<Value>>,
|
|
33
|
+
cookies: Option<Opaque<Value>>,
|
|
34
|
+
body: Option<Opaque<Value>>,
|
|
35
|
+
raw_body: Option<Opaque<Value>>,
|
|
36
|
+
params: Option<Opaque<Value>>,
|
|
37
|
+
to_h: Option<Opaque<Value>>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[magnus::wrap(class = "Spikard::Native::Request", free_immediately, mark)]
|
|
41
|
+
pub(crate) struct NativeRequest {
|
|
42
|
+
method: String,
|
|
43
|
+
path: String,
|
|
44
|
+
path_params: Arc<HashMap<String, String>>,
|
|
45
|
+
query_params: JsonValue,
|
|
46
|
+
raw_query_params: Arc<HashMap<String, Vec<String>>>,
|
|
47
|
+
body: JsonValue,
|
|
48
|
+
raw_body: Option<Bytes>,
|
|
49
|
+
headers: Arc<HashMap<String, String>>,
|
|
50
|
+
cookies: Arc<HashMap<String, String>>,
|
|
51
|
+
validated_params: Option<JsonValue>,
|
|
52
|
+
cache: RefCell<RequestCache>,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static KEY_METHOD: LazyId = LazyId::new("method");
|
|
56
|
+
static KEY_PATH: LazyId = LazyId::new("path");
|
|
57
|
+
static KEY_PATH_PARAMS: LazyId = LazyId::new("path_params");
|
|
58
|
+
static KEY_QUERY: LazyId = LazyId::new("query");
|
|
59
|
+
static KEY_RAW_QUERY: LazyId = LazyId::new("raw_query");
|
|
60
|
+
static KEY_HEADERS: LazyId = LazyId::new("headers");
|
|
61
|
+
static KEY_COOKIES: LazyId = LazyId::new("cookies");
|
|
62
|
+
static KEY_BODY: LazyId = LazyId::new("body");
|
|
63
|
+
static KEY_RAW_BODY: LazyId = LazyId::new("raw_body");
|
|
64
|
+
static KEY_PARAMS: LazyId = LazyId::new("params");
|
|
65
|
+
|
|
66
|
+
impl NativeRequest {
|
|
67
|
+
pub(crate) fn from_request_data(request_data: RequestData, validated_params: Option<JsonValue>) -> Self {
|
|
68
|
+
let RequestData {
|
|
69
|
+
path_params,
|
|
70
|
+
query_params,
|
|
71
|
+
raw_query_params,
|
|
72
|
+
body,
|
|
73
|
+
raw_body,
|
|
74
|
+
headers,
|
|
75
|
+
cookies,
|
|
76
|
+
method,
|
|
77
|
+
path,
|
|
78
|
+
..
|
|
79
|
+
} = request_data;
|
|
80
|
+
|
|
81
|
+
Self {
|
|
82
|
+
method,
|
|
83
|
+
path,
|
|
84
|
+
path_params,
|
|
85
|
+
query_params,
|
|
86
|
+
raw_query_params,
|
|
87
|
+
body,
|
|
88
|
+
raw_body,
|
|
89
|
+
headers,
|
|
90
|
+
cookies,
|
|
91
|
+
validated_params,
|
|
92
|
+
cache: RefCell::new(RequestCache::default()),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn cache_get(cache: &Option<Opaque<Value>>, ruby: &Ruby) -> Option<Value> {
|
|
97
|
+
cache.as_ref().map(|v| v.get_inner_with(ruby))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn cache_set(slot: &mut Option<Opaque<Value>>, value: Value) -> Value {
|
|
101
|
+
*slot = Some(Opaque::from(value));
|
|
102
|
+
value
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pub(crate) fn method(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
106
|
+
if let Some(value) = {
|
|
107
|
+
let cache = this.cache.borrow();
|
|
108
|
+
Self::cache_get(&cache.method, ruby)
|
|
109
|
+
} {
|
|
110
|
+
return Ok(value);
|
|
111
|
+
}
|
|
112
|
+
let value = ruby.str_new(&this.method).as_value();
|
|
113
|
+
let mut cache = this.cache.borrow_mut();
|
|
114
|
+
Ok(Self::cache_set(&mut cache.method, value))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pub(crate) fn path(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
118
|
+
if let Some(value) = {
|
|
119
|
+
let cache = this.cache.borrow();
|
|
120
|
+
Self::cache_get(&cache.path, ruby)
|
|
121
|
+
} {
|
|
122
|
+
return Ok(value);
|
|
123
|
+
}
|
|
124
|
+
let value = ruby.str_new(&this.path).as_value();
|
|
125
|
+
let mut cache = this.cache.borrow_mut();
|
|
126
|
+
Ok(Self::cache_set(&mut cache.path, value))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub(crate) fn path_params(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
130
|
+
if let Some(cached) = {
|
|
131
|
+
let cache = this.cache.borrow();
|
|
132
|
+
Self::cache_get(&cache.path_params, ruby)
|
|
133
|
+
} {
|
|
134
|
+
return Ok(cached);
|
|
135
|
+
}
|
|
136
|
+
let value = map_to_ruby_hash(ruby, this.path_params.as_ref())?;
|
|
137
|
+
let mut cache = this.cache.borrow_mut();
|
|
138
|
+
Ok(Self::cache_set(&mut cache.path_params, value))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
pub(crate) fn query(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
142
|
+
if let Some(cached) = {
|
|
143
|
+
let cache = this.cache.borrow();
|
|
144
|
+
Self::cache_get(&cache.query, ruby)
|
|
145
|
+
} {
|
|
146
|
+
return Ok(cached);
|
|
147
|
+
}
|
|
148
|
+
let value = json_to_ruby(ruby, &this.query_params)?;
|
|
149
|
+
let mut cache = this.cache.borrow_mut();
|
|
150
|
+
Ok(Self::cache_set(&mut cache.query, value))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
pub(crate) fn raw_query(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
154
|
+
if let Some(cached) = {
|
|
155
|
+
let cache = this.cache.borrow();
|
|
156
|
+
Self::cache_get(&cache.raw_query, ruby)
|
|
157
|
+
} {
|
|
158
|
+
return Ok(cached);
|
|
159
|
+
}
|
|
160
|
+
let value = multimap_to_ruby_hash(ruby, this.raw_query_params.as_ref())?;
|
|
161
|
+
let mut cache = this.cache.borrow_mut();
|
|
162
|
+
Ok(Self::cache_set(&mut cache.raw_query, value))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pub(crate) fn headers(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
166
|
+
if let Some(cached) = {
|
|
167
|
+
let cache = this.cache.borrow();
|
|
168
|
+
Self::cache_get(&cache.headers, ruby)
|
|
169
|
+
} {
|
|
170
|
+
return Ok(cached);
|
|
171
|
+
}
|
|
172
|
+
let value = map_to_ruby_hash(ruby, this.headers.as_ref())?;
|
|
173
|
+
let mut cache = this.cache.borrow_mut();
|
|
174
|
+
Ok(Self::cache_set(&mut cache.headers, value))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
pub(crate) fn cookies(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
178
|
+
if let Some(cached) = {
|
|
179
|
+
let cache = this.cache.borrow();
|
|
180
|
+
Self::cache_get(&cache.cookies, ruby)
|
|
181
|
+
} {
|
|
182
|
+
return Ok(cached);
|
|
183
|
+
}
|
|
184
|
+
let value = map_to_ruby_hash(ruby, this.cookies.as_ref())?;
|
|
185
|
+
let mut cache = this.cache.borrow_mut();
|
|
186
|
+
Ok(Self::cache_set(&mut cache.cookies, value))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
pub(crate) fn body(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
190
|
+
if let Some(cached) = {
|
|
191
|
+
let cache = this.cache.borrow();
|
|
192
|
+
Self::cache_get(&cache.body, ruby)
|
|
193
|
+
} {
|
|
194
|
+
return Ok(cached);
|
|
195
|
+
}
|
|
196
|
+
let value = json_to_ruby(ruby, &this.body)?;
|
|
197
|
+
let mut cache = this.cache.borrow_mut();
|
|
198
|
+
Ok(Self::cache_set(&mut cache.body, value))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
pub(crate) fn raw_body(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
202
|
+
if let Some(cached) = {
|
|
203
|
+
let cache = this.cache.borrow();
|
|
204
|
+
Self::cache_get(&cache.raw_body, ruby)
|
|
205
|
+
} {
|
|
206
|
+
return Ok(cached);
|
|
207
|
+
}
|
|
208
|
+
let value = match &this.raw_body {
|
|
209
|
+
Some(bytes) => ruby.str_from_slice(bytes.as_ref()).as_value(),
|
|
210
|
+
None => ruby.qnil().as_value(),
|
|
211
|
+
};
|
|
212
|
+
let mut cache = this.cache.borrow_mut();
|
|
213
|
+
Ok(Self::cache_set(&mut cache.raw_body, value))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pub(crate) fn params(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
217
|
+
if let Some(value) = {
|
|
218
|
+
let cache = this.cache.borrow();
|
|
219
|
+
Self::cache_get(&cache.params, ruby)
|
|
220
|
+
} {
|
|
221
|
+
return Ok(value);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let value = if let Some(validated) = &this.validated_params {
|
|
225
|
+
json_to_ruby(ruby, validated)?
|
|
226
|
+
} else {
|
|
227
|
+
let params = ruby.hash_new();
|
|
228
|
+
if let Some(hash) = RHash::from_value(Self::path_params(ruby, this)?) {
|
|
229
|
+
let _: Value = params.funcall("merge!", (hash,))?;
|
|
230
|
+
}
|
|
231
|
+
if let Some(hash) = RHash::from_value(Self::query(ruby, this)?) {
|
|
232
|
+
let _: Value = params.funcall("merge!", (hash,))?;
|
|
233
|
+
}
|
|
234
|
+
if let Some(hash) = RHash::from_value(Self::headers(ruby, this)?) {
|
|
235
|
+
let _: Value = params.funcall("merge!", (hash,))?;
|
|
236
|
+
}
|
|
237
|
+
if let Some(hash) = RHash::from_value(Self::cookies(ruby, this)?) {
|
|
238
|
+
let _: Value = params.funcall("merge!", (hash,))?;
|
|
239
|
+
}
|
|
240
|
+
params.as_value()
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
let mut cache = this.cache.borrow_mut();
|
|
244
|
+
Ok(Self::cache_set(&mut cache.params, value))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
pub(crate) fn to_h(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
248
|
+
if let Some(value) = {
|
|
249
|
+
let cache = this.cache.borrow();
|
|
250
|
+
Self::cache_get(&cache.to_h, ruby)
|
|
251
|
+
} {
|
|
252
|
+
return Ok(value);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let hash = ruby.hash_new_capa(10);
|
|
256
|
+
hash.aset(ruby.intern("method"), Self::method(ruby, this)?)?;
|
|
257
|
+
hash.aset(ruby.intern("path"), Self::path(ruby, this)?)?;
|
|
258
|
+
hash.aset(ruby.intern("path_params"), Self::path_params(ruby, this)?)?;
|
|
259
|
+
hash.aset(ruby.intern("query"), Self::query(ruby, this)?)?;
|
|
260
|
+
hash.aset(ruby.intern("raw_query"), Self::raw_query(ruby, this)?)?;
|
|
261
|
+
hash.aset(ruby.intern("headers"), Self::headers(ruby, this)?)?;
|
|
262
|
+
hash.aset(ruby.intern("cookies"), Self::cookies(ruby, this)?)?;
|
|
263
|
+
hash.aset(ruby.intern("body"), Self::body(ruby, this)?)?;
|
|
264
|
+
hash.aset(ruby.intern("raw_body"), Self::raw_body(ruby, this)?)?;
|
|
265
|
+
hash.aset(ruby.intern("params"), Self::params(ruby, this)?)?;
|
|
266
|
+
|
|
267
|
+
let mut cache = this.cache.borrow_mut();
|
|
268
|
+
Ok(Self::cache_set(&mut cache.to_h, hash.as_value()))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
pub(crate) fn index(ruby: &Ruby, this: &Self, key: Value) -> Result<Value, Error> {
|
|
272
|
+
if let Ok(sym) = Symbol::try_convert(key) {
|
|
273
|
+
return if sym == KEY_METHOD {
|
|
274
|
+
Self::method(ruby, this)
|
|
275
|
+
} else if sym == KEY_PATH {
|
|
276
|
+
Self::path(ruby, this)
|
|
277
|
+
} else if sym == KEY_PATH_PARAMS {
|
|
278
|
+
Self::path_params(ruby, this)
|
|
279
|
+
} else if sym == KEY_QUERY {
|
|
280
|
+
Self::query(ruby, this)
|
|
281
|
+
} else if sym == KEY_RAW_QUERY {
|
|
282
|
+
Self::raw_query(ruby, this)
|
|
283
|
+
} else if sym == KEY_HEADERS {
|
|
284
|
+
Self::headers(ruby, this)
|
|
285
|
+
} else if sym == KEY_COOKIES {
|
|
286
|
+
Self::cookies(ruby, this)
|
|
287
|
+
} else if sym == KEY_BODY {
|
|
288
|
+
Self::body(ruby, this)
|
|
289
|
+
} else if sym == KEY_RAW_BODY {
|
|
290
|
+
Self::raw_body(ruby, this)
|
|
291
|
+
} else if sym == KEY_PARAMS {
|
|
292
|
+
Self::params(ruby, this)
|
|
293
|
+
} else {
|
|
294
|
+
Ok(ruby.qnil().as_value())
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if let Ok(text) = RString::try_convert(key) {
|
|
299
|
+
let slice = unsafe { text.as_slice() };
|
|
300
|
+
return match slice {
|
|
301
|
+
b"method" => Self::method(ruby, this),
|
|
302
|
+
b"path" => Self::path(ruby, this),
|
|
303
|
+
b"path_params" => Self::path_params(ruby, this),
|
|
304
|
+
b"query" => Self::query(ruby, this),
|
|
305
|
+
b"raw_query" => Self::raw_query(ruby, this),
|
|
306
|
+
b"headers" => Self::headers(ruby, this),
|
|
307
|
+
b"cookies" => Self::cookies(ruby, this),
|
|
308
|
+
b"body" => Self::body(ruby, this),
|
|
309
|
+
b"raw_body" => Self::raw_body(ruby, this),
|
|
310
|
+
b"params" => Self::params(ruby, this),
|
|
311
|
+
_ => Ok(ruby.qnil().as_value()),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
Ok(ruby.qnil().as_value())
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#[allow(dead_code)]
|
|
319
|
+
pub(crate) fn mark(&self, marker: &Marker) {
|
|
320
|
+
if let Ok(ruby) = Ruby::get() {
|
|
321
|
+
let cache = self.cache.borrow();
|
|
322
|
+
for handle in [
|
|
323
|
+
&cache.method,
|
|
324
|
+
&cache.path,
|
|
325
|
+
&cache.path_params,
|
|
326
|
+
&cache.query,
|
|
327
|
+
&cache.raw_query,
|
|
328
|
+
&cache.headers,
|
|
329
|
+
&cache.cookies,
|
|
330
|
+
&cache.body,
|
|
331
|
+
&cache.raw_body,
|
|
332
|
+
&cache.params,
|
|
333
|
+
&cache.to_h,
|
|
334
|
+
]
|
|
335
|
+
.into_iter()
|
|
336
|
+
.filter_map(|value| value.as_ref())
|
|
337
|
+
{
|
|
338
|
+
marker.mark(handle.get_inner_with(&ruby));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -197,14 +197,7 @@ pub fn run_server(
|
|
|
197
197
|
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "WebSocket handlers must be a Hash"))?;
|
|
198
198
|
|
|
199
199
|
ws_hash.foreach(|path: String, factory: Value| -> Result<ForEach, Error> {
|
|
200
|
-
let
|
|
201
|
-
Error::new(
|
|
202
|
-
ruby.exception_runtime_error(),
|
|
203
|
-
format!("Failed to create WebSocket handler: {}", e),
|
|
204
|
-
)
|
|
205
|
-
})?;
|
|
206
|
-
|
|
207
|
-
let ws_state = crate::websocket::create_websocket_state(ruby, handler_instance)?;
|
|
200
|
+
let ws_state = crate::websocket::create_websocket_state(ruby, factory)?;
|
|
208
201
|
|
|
209
202
|
ws_endpoints.push((path, ws_state));
|
|
210
203
|
|
|
@@ -204,14 +204,7 @@ pub fn run_server(
|
|
|
204
204
|
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "WebSocket handlers must be a Hash"))?;
|
|
205
205
|
|
|
206
206
|
ws_hash.foreach(|path: String, factory: Value| -> Result<ForEach, Error> {
|
|
207
|
-
let
|
|
208
|
-
Error::new(
|
|
209
|
-
ruby.exception_runtime_error(),
|
|
210
|
-
format!("Failed to create WebSocket handler: {}", e),
|
|
211
|
-
)
|
|
212
|
-
})?;
|
|
213
|
-
|
|
214
|
-
let ws_state = crate::websocket::create_websocket_state(ruby, handler_instance)?;
|
|
207
|
+
let ws_state = crate::websocket::create_websocket_state(ruby, factory)?;
|
|
215
208
|
|
|
216
209
|
ws_endpoints.push((path, ws_state));
|
|
217
210
|
|
|
@@ -153,14 +153,7 @@ impl NativeTestClient {
|
|
|
153
153
|
|
|
154
154
|
ws_hash.foreach(
|
|
155
155
|
|path: String, factory: Value| -> Result<magnus::r_hash::ForEach, Error> {
|
|
156
|
-
let
|
|
157
|
-
Error::new(
|
|
158
|
-
ruby.exception_runtime_error(),
|
|
159
|
-
format!("Failed to create WebSocket handler: {}", e),
|
|
160
|
-
)
|
|
161
|
-
})?;
|
|
162
|
-
|
|
163
|
-
let ws_state = crate::websocket::create_websocket_state(ruby, handler_instance)?;
|
|
156
|
+
let ws_state = crate::websocket::create_websocket_state(ruby, factory)?;
|
|
164
157
|
|
|
165
158
|
ws_endpoints.push((path, ws_state));
|
|
166
159
|
|
|
@@ -360,6 +353,122 @@ impl NativeTestClient {
|
|
|
360
353
|
crate::testing::sse::sse_stream_from_response(ruby, &snapshot)
|
|
361
354
|
}
|
|
362
355
|
|
|
356
|
+
/// Send a GraphQL query/mutation
|
|
357
|
+
pub fn graphql(
|
|
358
|
+
ruby: &Ruby,
|
|
359
|
+
this: &Self,
|
|
360
|
+
query: String,
|
|
361
|
+
variables: Value,
|
|
362
|
+
operation_name: Value,
|
|
363
|
+
) -> Result<Value, Error> {
|
|
364
|
+
let inner_borrow = this.inner.borrow();
|
|
365
|
+
let inner = inner_borrow
|
|
366
|
+
.as_ref()
|
|
367
|
+
.ok_or_else(|| Error::new(ruby.exception_runtime_error(), "TestClient not initialised"))?;
|
|
368
|
+
|
|
369
|
+
let json_module = ruby
|
|
370
|
+
.class_object()
|
|
371
|
+
.const_get("JSON")
|
|
372
|
+
.map_err(|_| Error::new(ruby.exception_runtime_error(), "JSON module not available"))?;
|
|
373
|
+
|
|
374
|
+
let variables_json = if variables.is_nil() {
|
|
375
|
+
None
|
|
376
|
+
} else {
|
|
377
|
+
Some(crate::conversion::ruby_value_to_json(ruby, json_module, variables)?)
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
let operation_name_str = if operation_name.is_nil() {
|
|
381
|
+
None
|
|
382
|
+
} else {
|
|
383
|
+
Some(String::try_convert(operation_name)?)
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
let runtime = crate::server::global_runtime(ruby)?;
|
|
387
|
+
let server = inner.http_server.clone();
|
|
388
|
+
let query_value = query.clone();
|
|
389
|
+
|
|
390
|
+
let snapshot = crate::call_without_gvl!(
|
|
391
|
+
block_on_graphql,
|
|
392
|
+
args: (
|
|
393
|
+
runtime, &tokio::runtime::Runtime,
|
|
394
|
+
server, Arc<TestServer>,
|
|
395
|
+
query_value, String,
|
|
396
|
+
variables_json, Option<JsonValue>,
|
|
397
|
+
operation_name_str, Option<String>
|
|
398
|
+
),
|
|
399
|
+
return_type: Result<ResponseSnapshot, NativeRequestError>
|
|
400
|
+
)
|
|
401
|
+
.map_err(|err| {
|
|
402
|
+
Error::new(
|
|
403
|
+
ruby.exception_runtime_error(),
|
|
404
|
+
format!("GraphQL request failed: {}", err.0),
|
|
405
|
+
)
|
|
406
|
+
})?;
|
|
407
|
+
|
|
408
|
+
response_snapshot_to_ruby(ruby, snapshot)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/// Send a GraphQL query and get HTTP status separately
|
|
412
|
+
pub fn graphql_with_status(
|
|
413
|
+
ruby: &Ruby,
|
|
414
|
+
this: &Self,
|
|
415
|
+
query: String,
|
|
416
|
+
variables: Value,
|
|
417
|
+
operation_name: Value,
|
|
418
|
+
) -> Result<Value, Error> {
|
|
419
|
+
let inner_borrow = this.inner.borrow();
|
|
420
|
+
let inner = inner_borrow
|
|
421
|
+
.as_ref()
|
|
422
|
+
.ok_or_else(|| Error::new(ruby.exception_runtime_error(), "TestClient not initialised"))?;
|
|
423
|
+
|
|
424
|
+
let json_module = ruby
|
|
425
|
+
.class_object()
|
|
426
|
+
.const_get("JSON")
|
|
427
|
+
.map_err(|_| Error::new(ruby.exception_runtime_error(), "JSON module not available"))?;
|
|
428
|
+
|
|
429
|
+
let variables_json = if variables.is_nil() {
|
|
430
|
+
None
|
|
431
|
+
} else {
|
|
432
|
+
Some(crate::conversion::ruby_value_to_json(ruby, json_module, variables)?)
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
let operation_name_str = if operation_name.is_nil() {
|
|
436
|
+
None
|
|
437
|
+
} else {
|
|
438
|
+
Some(String::try_convert(operation_name)?)
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
let runtime = crate::server::global_runtime(ruby)?;
|
|
442
|
+
let server = inner.http_server.clone();
|
|
443
|
+
let query_value = query.clone();
|
|
444
|
+
|
|
445
|
+
let snapshot = crate::call_without_gvl!(
|
|
446
|
+
block_on_graphql,
|
|
447
|
+
args: (
|
|
448
|
+
runtime, &tokio::runtime::Runtime,
|
|
449
|
+
server, Arc<TestServer>,
|
|
450
|
+
query_value, String,
|
|
451
|
+
variables_json, Option<JsonValue>,
|
|
452
|
+
operation_name_str, Option<String>
|
|
453
|
+
),
|
|
454
|
+
return_type: Result<ResponseSnapshot, NativeRequestError>
|
|
455
|
+
)
|
|
456
|
+
.map_err(|err| {
|
|
457
|
+
Error::new(
|
|
458
|
+
ruby.exception_runtime_error(),
|
|
459
|
+
format!("GraphQL request failed: {}", err.0),
|
|
460
|
+
)
|
|
461
|
+
})?;
|
|
462
|
+
|
|
463
|
+
let status = snapshot.status;
|
|
464
|
+
let response = response_snapshot_to_ruby(ruby, snapshot)?;
|
|
465
|
+
|
|
466
|
+
let array = ruby.ary_new_capa(2);
|
|
467
|
+
array.push(ruby.integer_from_i64(status as i64))?;
|
|
468
|
+
array.push(response)?;
|
|
469
|
+
Ok(array.as_value())
|
|
470
|
+
}
|
|
471
|
+
|
|
363
472
|
/// GC mark hook so Ruby keeps handler closures alive.
|
|
364
473
|
#[allow(dead_code)]
|
|
365
474
|
pub fn mark(&self, marker: &Marker) {
|
|
@@ -381,7 +490,7 @@ fn websocket_timeout() -> Duration {
|
|
|
381
490
|
Duration::from_millis(timeout_ms)
|
|
382
491
|
}
|
|
383
492
|
|
|
384
|
-
fn block_on_request(
|
|
493
|
+
pub fn block_on_request(
|
|
385
494
|
runtime: &tokio::runtime::Runtime,
|
|
386
495
|
server: Arc<TestServer>,
|
|
387
496
|
method: Method,
|
|
@@ -536,3 +645,53 @@ pub async fn execute_request(
|
|
|
536
645
|
fn snapshot_err_to_native(err: SnapshotError) -> NativeRequestError {
|
|
537
646
|
NativeRequestError(err.to_string())
|
|
538
647
|
}
|
|
648
|
+
|
|
649
|
+
pub fn block_on_graphql(
|
|
650
|
+
runtime: &tokio::runtime::Runtime,
|
|
651
|
+
server: Arc<TestServer>,
|
|
652
|
+
query: String,
|
|
653
|
+
variables: Option<JsonValue>,
|
|
654
|
+
operation_name: Option<String>,
|
|
655
|
+
) -> Result<ResponseSnapshot, NativeRequestError> {
|
|
656
|
+
runtime.block_on(execute_graphql_request(server, query, variables, operation_name))
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async fn execute_graphql_request(
|
|
660
|
+
server: Arc<TestServer>,
|
|
661
|
+
query: String,
|
|
662
|
+
variables: Option<JsonValue>,
|
|
663
|
+
operation_name: Option<String>,
|
|
664
|
+
) -> Result<ResponseSnapshot, NativeRequestError> {
|
|
665
|
+
let mut body = serde_json::json!({ "query": query });
|
|
666
|
+
if let Some(vars) = variables {
|
|
667
|
+
body["variables"] = vars;
|
|
668
|
+
}
|
|
669
|
+
if let Some(op_name) = operation_name {
|
|
670
|
+
body["operationName"] = JsonValue::String(op_name);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
let response = server.post("/graphql").json(&body).await;
|
|
674
|
+
let snapshot = snapshot_response(response).await.map_err(snapshot_err_to_native)?;
|
|
675
|
+
Ok(snapshot)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
fn response_snapshot_to_ruby(ruby: &Ruby, snapshot: ResponseSnapshot) -> Result<Value, Error> {
|
|
679
|
+
let hash = ruby.hash_new();
|
|
680
|
+
|
|
681
|
+
hash.aset(
|
|
682
|
+
ruby.intern("status_code"),
|
|
683
|
+
ruby.integer_from_i64(snapshot.status as i64),
|
|
684
|
+
)?;
|
|
685
|
+
|
|
686
|
+
let headers_hash = ruby.hash_new();
|
|
687
|
+
for (key, value) in snapshot.headers {
|
|
688
|
+
headers_hash.aset(ruby.str_new(&key), ruby.str_new(&value))?;
|
|
689
|
+
}
|
|
690
|
+
hash.aset(ruby.intern("headers"), headers_hash)?;
|
|
691
|
+
|
|
692
|
+
let body_value = ruby.str_new(&String::from_utf8_lossy(&snapshot.body));
|
|
693
|
+
hash.aset(ruby.intern("body"), body_value)?;
|
|
694
|
+
hash.aset(ruby.intern("body_text"), body_value)?;
|
|
695
|
+
|
|
696
|
+
Ok(hash.as_value())
|
|
697
|
+
}
|