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,136 @@
|
|
|
1
|
+
use brotli::Decompressor;
|
|
2
|
+
use flate2::read::GzDecoder;
|
|
3
|
+
use spikard_core::{CompressionConfig, RawResponse, StaticAsset};
|
|
4
|
+
use std::collections::HashMap;
|
|
5
|
+
use std::io::Read;
|
|
6
|
+
|
|
7
|
+
fn decode_gzip(payload: &[u8]) -> Vec<u8> {
|
|
8
|
+
let mut decoder = GzDecoder::new(payload);
|
|
9
|
+
let mut buf = Vec::new();
|
|
10
|
+
decoder.read_to_end(&mut buf).expect("gzip decode failed");
|
|
11
|
+
buf
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fn decode_brotli(payload: &[u8]) -> Vec<u8> {
|
|
15
|
+
let mut decoder = Decompressor::new(payload, 4096);
|
|
16
|
+
let mut buf = Vec::new();
|
|
17
|
+
decoder.read_to_end(&mut buf).expect("brotli decode failed");
|
|
18
|
+
buf
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[test]
|
|
22
|
+
fn raw_response_does_not_compress_empty_or_partial_content() {
|
|
23
|
+
let mut headers = HashMap::new();
|
|
24
|
+
headers.insert("content-type".to_string(), "application/json".to_string());
|
|
25
|
+
|
|
26
|
+
let mut empty = RawResponse::new(200, headers.clone(), Vec::new());
|
|
27
|
+
let cfg = CompressionConfig {
|
|
28
|
+
min_size: 1,
|
|
29
|
+
..Default::default()
|
|
30
|
+
};
|
|
31
|
+
empty.apply_compression(&HashMap::new(), &cfg);
|
|
32
|
+
assert!(!empty.headers.contains_key("content-encoding"));
|
|
33
|
+
assert!(empty.body.is_empty());
|
|
34
|
+
|
|
35
|
+
let mut partial = RawResponse::new(206, headers, b"{\"ok\":true}".to_vec());
|
|
36
|
+
partial.apply_compression(&HashMap::new(), &cfg);
|
|
37
|
+
assert!(!partial.headers.contains_key("content-encoding"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[test]
|
|
41
|
+
fn raw_response_respects_existing_content_encoding_and_min_size() {
|
|
42
|
+
let mut headers = HashMap::new();
|
|
43
|
+
headers.insert("content-encoding".to_string(), "gzip".to_string());
|
|
44
|
+
|
|
45
|
+
let mut response = RawResponse::new(200, headers, b"x".repeat(4096));
|
|
46
|
+
let cfg = CompressionConfig::default();
|
|
47
|
+
response.apply_compression(&HashMap::new(), &cfg);
|
|
48
|
+
assert_eq!(
|
|
49
|
+
response.headers.get("content-encoding").map(String::as_str),
|
|
50
|
+
Some("gzip")
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let mut small = RawResponse::new(200, HashMap::new(), b"x".repeat(10));
|
|
54
|
+
let cfg = CompressionConfig {
|
|
55
|
+
min_size: 1024,
|
|
56
|
+
..Default::default()
|
|
57
|
+
};
|
|
58
|
+
small.apply_compression(&HashMap::new(), &cfg);
|
|
59
|
+
assert!(!small.headers.contains_key("content-encoding"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[test]
|
|
63
|
+
fn raw_response_prefers_brotli_when_accepted() {
|
|
64
|
+
let original = b"hello world ".repeat(256);
|
|
65
|
+
let mut response = RawResponse::new(200, HashMap::new(), original.clone());
|
|
66
|
+
|
|
67
|
+
let mut request_headers = HashMap::new();
|
|
68
|
+
request_headers.insert("Accept-Encoding".to_string(), "br, gzip".to_string());
|
|
69
|
+
|
|
70
|
+
let cfg = CompressionConfig {
|
|
71
|
+
min_size: 1,
|
|
72
|
+
quality: 6,
|
|
73
|
+
gzip: true,
|
|
74
|
+
brotli: true,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
response.apply_compression(&request_headers, &cfg);
|
|
78
|
+
|
|
79
|
+
assert_eq!(response.headers.get("content-encoding").map(String::as_str), Some("br"));
|
|
80
|
+
assert_eq!(
|
|
81
|
+
response.headers.get("vary").map(String::as_str),
|
|
82
|
+
Some("Accept-Encoding")
|
|
83
|
+
);
|
|
84
|
+
assert_eq!(decode_brotli(&response.body), original);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn raw_response_falls_back_to_gzip() {
|
|
89
|
+
let original = b"hello world ".repeat(256);
|
|
90
|
+
let mut response = RawResponse::new(200, HashMap::new(), original.clone());
|
|
91
|
+
|
|
92
|
+
let mut request_headers = HashMap::new();
|
|
93
|
+
request_headers.insert("Accept-Encoding".to_string(), "gzip".to_string());
|
|
94
|
+
|
|
95
|
+
let cfg = CompressionConfig {
|
|
96
|
+
min_size: 1,
|
|
97
|
+
quality: 6,
|
|
98
|
+
gzip: true,
|
|
99
|
+
brotli: true,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
response.apply_compression(&request_headers, &cfg);
|
|
103
|
+
|
|
104
|
+
assert_eq!(
|
|
105
|
+
response.headers.get("content-encoding").map(String::as_str),
|
|
106
|
+
Some("gzip")
|
|
107
|
+
);
|
|
108
|
+
assert_eq!(decode_gzip(&response.body), original);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn static_asset_serves_get_and_head_only() {
|
|
113
|
+
let mut headers = HashMap::new();
|
|
114
|
+
headers.insert("content-type".to_string(), "text/plain".to_string());
|
|
115
|
+
|
|
116
|
+
let asset = StaticAsset {
|
|
117
|
+
route: "/static/hello.txt".to_string(),
|
|
118
|
+
headers,
|
|
119
|
+
body: b"hello".to_vec(),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
let get = asset.serve("GET", "/static/hello.txt").expect("expected GET response");
|
|
123
|
+
assert_eq!(get.status, 200);
|
|
124
|
+
assert_eq!(get.body, b"hello");
|
|
125
|
+
assert_eq!(get.headers.get("content-length").map(String::as_str), Some("5"));
|
|
126
|
+
|
|
127
|
+
let head = asset
|
|
128
|
+
.serve("HEAD", "/static/hello.txt")
|
|
129
|
+
.expect("expected HEAD response");
|
|
130
|
+
assert_eq!(head.status, 200);
|
|
131
|
+
assert!(head.body.is_empty());
|
|
132
|
+
assert_eq!(head.headers.get("content-length").map(String::as_str), Some("5"));
|
|
133
|
+
|
|
134
|
+
assert!(asset.serve("POST", "/static/hello.txt").is_none());
|
|
135
|
+
assert!(asset.serve("GET", "/static/other.txt").is_none());
|
|
136
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#![cfg(feature = "di")]
|
|
2
|
+
|
|
3
|
+
use http::Request;
|
|
4
|
+
use spikard_core::RequestData;
|
|
5
|
+
use spikard_core::di::{Dependency, DependencyError, ResolvedDependencies};
|
|
6
|
+
use std::any::Any;
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
struct DummyDependency;
|
|
10
|
+
|
|
11
|
+
impl Dependency for DummyDependency {
|
|
12
|
+
fn resolve(
|
|
13
|
+
&self,
|
|
14
|
+
_request: &Request<()>,
|
|
15
|
+
_request_data: &RequestData,
|
|
16
|
+
_resolved: &ResolvedDependencies,
|
|
17
|
+
) -> std::pin::Pin<
|
|
18
|
+
Box<dyn std::future::Future<Output = Result<Arc<dyn Any + Send + Sync>, DependencyError>> + Send + '_>,
|
|
19
|
+
> {
|
|
20
|
+
Box::pin(async move { Ok(Arc::new(()) as Arc<dyn Any + Send + Sync>) })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn key(&self) -> &str {
|
|
24
|
+
"dummy"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn depends_on(&self) -> Vec<String> {
|
|
28
|
+
Vec::new()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[test]
|
|
33
|
+
fn dependency_defaults_are_false() {
|
|
34
|
+
let dep = DummyDependency;
|
|
35
|
+
assert!(!dep.cacheable());
|
|
36
|
+
assert!(!dep.singleton());
|
|
37
|
+
}
|