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
data/ext/spikard_rb/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "spikard-rb-ext"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.2"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
|
|
@@ -13,5 +13,5 @@ crate-type = ["cdylib"]
|
|
|
13
13
|
|
|
14
14
|
[dependencies]
|
|
15
15
|
magnus = { version = "0.8.2", features = ["rb-sys"] }
|
|
16
|
-
# Use
|
|
16
|
+
# Use crates from the workspace root
|
|
17
17
|
spikard_rb_core = { package = "spikard-rb", path = "../../vendor/crates/spikard-rb" }
|
data/ext/spikard_rb/extconf.rb
CHANGED
|
@@ -7,7 +7,8 @@ default_profile = ENV.fetch('CARGO_PROFILE', 'release')
|
|
|
7
7
|
|
|
8
8
|
create_rust_makefile('spikard_rb') do |config|
|
|
9
9
|
config.profile = default_profile.to_sym
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
10
|
+
# Always use --locked to maintain consistency with vendored crates.
|
|
11
|
+
# The vendor-crates.sh script patches Cargo.toml files and relies on a
|
|
12
|
+
# committed Cargo.lock to avoid workspace collision issues during builds.
|
|
13
|
+
config.extra_cargo_args = ['--locked']
|
|
13
14
|
end
|
data/lib/spikard/config.rb
CHANGED
|
@@ -21,10 +21,34 @@ module Spikard
|
|
|
21
21
|
# @param min_size [Integer] Minimum response size in bytes to compress (default: 1024)
|
|
22
22
|
# @param quality [Integer] Compression quality level (0-11 for brotli, 0-9 for gzip, default: 6)
|
|
23
23
|
def initialize(gzip: true, brotli: true, min_size: 1024, quality: 6)
|
|
24
|
-
@gzip = gzip
|
|
25
|
-
@brotli = brotli
|
|
26
|
-
@min_size = min_size
|
|
27
|
-
@quality = quality
|
|
24
|
+
@gzip = normalize_boolean('gzip', gzip)
|
|
25
|
+
@brotli = normalize_boolean('brotli', brotli)
|
|
26
|
+
@min_size = normalize_nonnegative_integer('min_size', min_size)
|
|
27
|
+
@quality = normalize_quality(quality)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def normalize_boolean(name, value)
|
|
33
|
+
return value if [true, false].include?(value)
|
|
34
|
+
|
|
35
|
+
raise ArgumentError, "#{name} must be a boolean"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def normalize_nonnegative_integer(name, value)
|
|
39
|
+
raise ArgumentError, "#{name} must be an Integer" unless value.is_a?(Integer)
|
|
40
|
+
return value if value >= 0
|
|
41
|
+
|
|
42
|
+
raise ArgumentError, "#{name} must be >= 0"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def normalize_quality(value)
|
|
46
|
+
raise ArgumentError, 'quality must be a number' unless value.is_a?(Integer) || value.is_a?(Float)
|
|
47
|
+
|
|
48
|
+
normalized = value.to_i
|
|
49
|
+
return normalized if normalized.between?(0, 11)
|
|
50
|
+
|
|
51
|
+
raise ArgumentError, 'quality must be between 0 and 11'
|
|
28
52
|
end
|
|
29
53
|
end
|
|
30
54
|
|
|
@@ -378,19 +402,71 @@ module Spikard
|
|
|
378
402
|
openapi: nil
|
|
379
403
|
)
|
|
380
404
|
@host = host
|
|
381
|
-
@port = port
|
|
382
|
-
@workers = workers
|
|
383
|
-
@enable_request_id = enable_request_id
|
|
384
|
-
@max_body_size = max_body_size
|
|
385
|
-
@request_timeout = request_timeout
|
|
405
|
+
@port = normalize_port(port)
|
|
406
|
+
@workers = normalize_workers(workers)
|
|
407
|
+
@enable_request_id = normalize_boolean('enable_request_id', enable_request_id)
|
|
408
|
+
@max_body_size = normalize_optional_nonnegative_integer('max_body_size', max_body_size)
|
|
409
|
+
@request_timeout = normalize_timeout('request_timeout', request_timeout)
|
|
386
410
|
@compression = compression
|
|
387
411
|
@rate_limit = rate_limit
|
|
388
412
|
@jwt_auth = jwt_auth
|
|
389
413
|
@api_key_auth = api_key_auth
|
|
390
|
-
@static_files = static_files
|
|
391
|
-
@graceful_shutdown = graceful_shutdown
|
|
392
|
-
@shutdown_timeout = shutdown_timeout
|
|
414
|
+
@static_files = normalize_static_files(static_files)
|
|
415
|
+
@graceful_shutdown = normalize_boolean('graceful_shutdown', graceful_shutdown)
|
|
416
|
+
@shutdown_timeout = normalize_timeout('shutdown_timeout', shutdown_timeout)
|
|
393
417
|
@openapi = openapi
|
|
394
418
|
end
|
|
419
|
+
|
|
420
|
+
private
|
|
421
|
+
|
|
422
|
+
def normalize_port(port)
|
|
423
|
+
raise ArgumentError, 'port must be an Integer' unless port.is_a?(Integer)
|
|
424
|
+
return port if port.between?(1, 65_535)
|
|
425
|
+
|
|
426
|
+
raise ArgumentError, 'port must be between 1 and 65535'
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def normalize_workers(workers)
|
|
430
|
+
raise ArgumentError, 'workers must be an Integer' unless workers.is_a?(Integer)
|
|
431
|
+
return workers if workers >= 1
|
|
432
|
+
|
|
433
|
+
raise ArgumentError, 'workers must be >= 1'
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def normalize_boolean(name, value)
|
|
437
|
+
return value if [true, false].include?(value)
|
|
438
|
+
|
|
439
|
+
raise ArgumentError, "#{name} must be a boolean"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def normalize_optional_nonnegative_integer(name, value)
|
|
443
|
+
return nil if value.nil?
|
|
444
|
+
raise ArgumentError, "#{name} must be an Integer" unless value.is_a?(Integer)
|
|
445
|
+
return value if value >= 0
|
|
446
|
+
|
|
447
|
+
raise ArgumentError, "#{name} must be >= 0"
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def normalize_timeout(name, value)
|
|
451
|
+
return nil if value.nil?
|
|
452
|
+
raise ArgumentError, "#{name} must be a number" unless value.is_a?(Integer) || value.is_a?(Float)
|
|
453
|
+
|
|
454
|
+
normalized = value.to_i
|
|
455
|
+
return normalized if normalized >= 0
|
|
456
|
+
|
|
457
|
+
raise ArgumentError, "#{name} must be >= 0"
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def normalize_static_files(static_files)
|
|
461
|
+
return [] if static_files.nil?
|
|
462
|
+
raise ArgumentError, 'static_files must be an Array' unless static_files.is_a?(Array)
|
|
463
|
+
|
|
464
|
+
static_files.each do |entry|
|
|
465
|
+
next if entry.is_a?(StaticFilesConfig)
|
|
466
|
+
|
|
467
|
+
raise ArgumentError, 'static_files entries must be StaticFilesConfig'
|
|
468
|
+
end
|
|
469
|
+
static_files
|
|
470
|
+
end
|
|
395
471
|
end
|
|
396
472
|
end
|
data/lib/spikard/testing.rb
CHANGED
|
@@ -38,8 +38,10 @@ module Spikard
|
|
|
38
38
|
handlers = app.handler_map.transform_keys(&:to_sym)
|
|
39
39
|
ws_handlers = app.websocket_handlers || {}
|
|
40
40
|
sse_producers = app.sse_producers || {}
|
|
41
|
+
hooks = app.instance_variable_get(:@native_hooks)
|
|
41
42
|
dependencies = app.dependencies || {}
|
|
42
|
-
|
|
43
|
+
payload = { hooks: hooks, dependencies: dependencies }
|
|
44
|
+
Spikard::Native::TestClient.new(routes_json, handlers, config, ws_handlers, sse_producers, payload)
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
# High level wrapper around the native test client.
|
data/lib/spikard/version.rb
CHANGED
data/lib/spikard.rb
CHANGED
|
@@ -34,6 +34,17 @@ rescue LoadError => e
|
|
|
34
34
|
MSG
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
if defined?(Spikard::Native::TestClient) && !Spikard::Native::TestClient.method_defined?(:__spikard_native_request,
|
|
38
|
+
false)
|
|
39
|
+
Spikard::Native::TestClient.class_eval do
|
|
40
|
+
alias_method :__spikard_native_request, :request
|
|
41
|
+
|
|
42
|
+
def request(method, path, options = nil)
|
|
43
|
+
__spikard_native_request(method, path, options)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
37
48
|
# Convenience aliases and methods at top level
|
|
38
49
|
module Spikard
|
|
39
50
|
TestClient = Testing::TestClient
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "spikard-bindings-shared"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.2"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -18,6 +18,7 @@ nursery = { level = "deny", priority = 0 }
|
|
|
18
18
|
[dependencies]
|
|
19
19
|
serde = { version = "1.0", features = ["derive"] }
|
|
20
20
|
serde_json = "1.0"
|
|
21
|
+
simd-json = "0.17"
|
|
21
22
|
axum = { version = "0.8", features = ["multipart", "ws"] }
|
|
22
23
|
tokio = { version = "1", features = ["full"] }
|
|
23
24
|
thiserror = "2.0"
|
|
@@ -27,6 +28,7 @@ tracing = "0.1"
|
|
|
27
28
|
http = "1.4"
|
|
28
29
|
http-body-util = "0.1"
|
|
29
30
|
tonic = "0.14"
|
|
31
|
+
bytes = "1.11"
|
|
30
32
|
|
|
31
33
|
[features]
|
|
32
34
|
default = []
|
|
@@ -34,7 +36,6 @@ python-support = ["pyo3", "pyo3-async-runtimes"]
|
|
|
34
36
|
node-support = ["napi", "napi-derive"]
|
|
35
37
|
ruby-support = ["magnus"]
|
|
36
38
|
php-support = ["ext-php-rs"]
|
|
37
|
-
wasm-support = ["wasm-bindgen"]
|
|
38
39
|
|
|
39
40
|
[dependencies.pyo3]
|
|
40
41
|
version = "0.27"
|
|
@@ -64,9 +65,5 @@ optional = true
|
|
|
64
65
|
version = "0.15"
|
|
65
66
|
optional = true
|
|
66
67
|
|
|
67
|
-
[dependencies.wasm-bindgen]
|
|
68
|
-
version = "0.2"
|
|
69
|
-
optional = true
|
|
70
|
-
|
|
71
68
|
[dev-dependencies]
|
|
72
69
|
pretty_assertions = "1.4"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
//! Example of implementing ConfigSource for a language binding
|
|
1
|
+
//! Example of implementing `ConfigSource` for a language binding
|
|
2
2
|
//!
|
|
3
3
|
//! This example demonstrates how a language binding (e.g., Python, Node.js, Ruby, PHP)
|
|
4
|
-
//! would implement the `ConfigSource` trait to extract ServerConfig from language-specific objects.
|
|
4
|
+
//! would implement the `ConfigSource` trait to extract `ServerConfig` from language-specific objects.
|
|
5
5
|
|
|
6
6
|
use spikard_bindings_shared::{ConfigExtractor, ConfigSource};
|
|
7
7
|
use std::collections::HashMap;
|
|
8
8
|
|
|
9
|
-
/// Example: PyO3 Python dict wrapper
|
|
9
|
+
/// Example: `PyO3` Python dict wrapper
|
|
10
10
|
struct PyDictWrapper {
|
|
11
11
|
data: HashMap<String, String>,
|
|
12
12
|
}
|
|
@@ -74,7 +74,7 @@ fn main() {
|
|
|
74
74
|
println!(" min_size: {}", config.min_size);
|
|
75
75
|
println!(" quality: {}\n", config.quality);
|
|
76
76
|
}
|
|
77
|
-
Err(e) => println!(" Error: {}\n"
|
|
77
|
+
Err(e) => println!(" Error: {e}\n"),
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
println!("2. Extracting JWT authentication configuration:");
|
|
@@ -89,7 +89,7 @@ fn main() {
|
|
|
89
89
|
println!(" algorithm: {}", config.algorithm);
|
|
90
90
|
println!(" leeway: {}\n", config.leeway);
|
|
91
91
|
}
|
|
92
|
-
Err(e) => println!(" Error: {}\n"
|
|
92
|
+
Err(e) => println!(" Error: {e}\n"),
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
println!("3. Extracting API Key authentication configuration:");
|
|
@@ -102,7 +102,7 @@ fn main() {
|
|
|
102
102
|
println!(" keys: {:?}", config.keys);
|
|
103
103
|
println!(" header_name: {}\n", config.header_name);
|
|
104
104
|
}
|
|
105
|
-
Err(e) => println!(" Error: {}\n"
|
|
105
|
+
Err(e) => println!(" Error: {e}\n"),
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
println!("4. Extracting rate limit configuration:");
|
|
@@ -117,7 +117,7 @@ fn main() {
|
|
|
117
117
|
println!(" burst: {}", config.burst);
|
|
118
118
|
println!(" ip_based: {}\n", config.ip_based);
|
|
119
119
|
}
|
|
120
|
-
Err(e) => println!(" Error: {}\n"
|
|
120
|
+
Err(e) => println!(" Error: {e}\n"),
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
println!("5. Testing error handling (missing 'burst' field):");
|
|
@@ -125,7 +125,7 @@ fn main() {
|
|
|
125
125
|
|
|
126
126
|
match ConfigExtractor::extract_rate_limit_config(&rate_limit_config) {
|
|
127
127
|
Ok(_config) => println!(" Success (unexpected!)"),
|
|
128
|
-
Err(e) => println!(" Expected error: {}\n"
|
|
128
|
+
Err(e) => println!(" Expected error: {e}\n"),
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
println!("=== Example Complete ===");
|
|
@@ -587,7 +587,7 @@ mod tests {
|
|
|
587
587
|
assert_eq!(config.port, 3000);
|
|
588
588
|
assert_eq!(config.workers, 4);
|
|
589
589
|
assert!(!config.enable_request_id);
|
|
590
|
-
assert_eq!(config.max_body_size, Some(
|
|
590
|
+
assert_eq!(config.max_body_size, Some(5_242_880));
|
|
591
591
|
assert_eq!(config.request_timeout, Some(60));
|
|
592
592
|
assert!(!config.graceful_shutdown);
|
|
593
593
|
assert_eq!(config.shutdown_timeout, 10);
|
|
@@ -620,7 +620,7 @@ mod tests {
|
|
|
620
620
|
fn test_security_schemes_config_empty() {
|
|
621
621
|
let source = MockConfigSource::new();
|
|
622
622
|
|
|
623
|
-
let schemes = ConfigExtractor::extract_security_schemes_config(&source)
|
|
623
|
+
let schemes = ConfigExtractor::extract_security_schemes_config(&source);
|
|
624
624
|
assert_eq!(schemes.len(), 0);
|
|
625
625
|
}
|
|
626
626
|
|
|
@@ -87,7 +87,7 @@ mod tests {
|
|
|
87
87
|
fn from_any(value: &(dyn Any + Send + Sync)) -> Result<Self, Self::Error> {
|
|
88
88
|
value
|
|
89
89
|
.downcast_ref::<i32>()
|
|
90
|
-
.map(|&v|
|
|
90
|
+
.map(|&v| Self { value: v })
|
|
91
91
|
.ok_or_else(|| "Invalid type".to_string())
|
|
92
92
|
}
|
|
93
93
|
}
|
|
@@ -169,7 +169,7 @@ mod tests {
|
|
|
169
169
|
#[test]
|
|
170
170
|
fn test_json_null_conversion() {
|
|
171
171
|
let null_value = serde_json::Value::Null;
|
|
172
|
-
let result = serde_json::Value::from_json(null_value
|
|
172
|
+
let result = serde_json::Value::from_json(null_value);
|
|
173
173
|
assert!(result.is_ok());
|
|
174
174
|
assert!(result.unwrap().is_null());
|
|
175
175
|
}
|
|
@@ -177,7 +177,7 @@ mod tests {
|
|
|
177
177
|
#[test]
|
|
178
178
|
fn test_json_array_conversion() {
|
|
179
179
|
let array = json!([1, 2, 3, 4, 5]);
|
|
180
|
-
let result = serde_json::Value::from_json(array
|
|
180
|
+
let result = serde_json::Value::from_json(array);
|
|
181
181
|
assert!(result.is_ok());
|
|
182
182
|
let converted = result.unwrap();
|
|
183
183
|
assert!(converted.is_array());
|
|
@@ -196,7 +196,7 @@ mod tests {
|
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
-
let result = serde_json::Value::from_json(nested
|
|
199
|
+
let result = serde_json::Value::from_json(nested);
|
|
200
200
|
assert!(result.is_ok());
|
|
201
201
|
let converted = result.unwrap();
|
|
202
202
|
assert_eq!(converted["level1"]["level2"]["level3"]["value"], "deep");
|
|
@@ -19,21 +19,21 @@ type DependencyFuture<'a> =
|
|
|
19
19
|
/// Adapter trait for value dependencies across language bindings
|
|
20
20
|
///
|
|
21
21
|
/// Language bindings should implement this trait to wrap their
|
|
22
|
-
/// language-specific value storage (e.g., Py<PyAny
|
|
22
|
+
/// language-specific value storage (e.g., `Py<PyAny>`, `Opaque<Value>`, etc.)
|
|
23
23
|
pub trait ValueDependencyAdapter: Send + Sync {
|
|
24
24
|
/// Get the dependency key
|
|
25
25
|
fn key(&self) -> &str;
|
|
26
26
|
|
|
27
27
|
/// Resolve the stored value
|
|
28
28
|
///
|
|
29
|
-
/// Returns an Arc<dyn Any
|
|
29
|
+
/// Returns an `Arc<dyn Any>` that can be downcast to the concrete type
|
|
30
30
|
fn resolve_value(&self) -> DependencyFuture<'_>;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/// Adapter trait for factory dependencies across language bindings
|
|
34
34
|
///
|
|
35
35
|
/// Language bindings should implement this trait to wrap their
|
|
36
|
-
/// language-specific callable storage (e.g., Py<PyAny
|
|
36
|
+
/// language-specific callable storage (e.g., `Py<PyAny>`, `ThreadsafeFunction`, etc.)
|
|
37
37
|
pub trait FactoryDependencyAdapter: Send + Sync {
|
|
38
38
|
/// Get the dependency key
|
|
39
39
|
fn key(&self) -> &str;
|
|
@@ -41,7 +41,7 @@ pub trait FactoryDependencyAdapter: Send + Sync {
|
|
|
41
41
|
/// Invoke the factory with resolved dependencies
|
|
42
42
|
///
|
|
43
43
|
/// The factory receives already-resolved dependencies and returns
|
|
44
|
-
/// a new instance wrapped in Arc<dyn Any
|
|
44
|
+
/// a new instance wrapped in `Arc<dyn Any>`
|
|
45
45
|
fn invoke_factory(
|
|
46
46
|
&self,
|
|
47
47
|
request: &Request<()>,
|
|
@@ -81,6 +81,9 @@ impl<T: ValueDependencyAdapter + 'static> Dependency for ValueDependencyBridge<T
|
|
|
81
81
|
self.adapter.key()
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// PERFORMANCE: Value dependencies have no sub-dependencies.
|
|
85
|
+
// Returning an empty Vec is unavoidable if the trait requires it, but
|
|
86
|
+
// consider optimizing the DI system to use Option<&[String]> or a default method.
|
|
84
87
|
fn depends_on(&self) -> Vec<String> {
|
|
85
88
|
vec![]
|
|
86
89
|
}
|
|
@@ -114,6 +117,9 @@ impl<T: FactoryDependencyAdapter + 'static> Dependency for FactoryDependencyBrid
|
|
|
114
117
|
self.adapter.key()
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
// PERFORMANCE: Factory dependencies may or may not have sub-dependencies.
|
|
121
|
+
// Language bindings should override this if they track dependencies.
|
|
122
|
+
// The default empty Vec is acceptable for language bindings that don't track explicit dependency graphs.
|
|
117
123
|
fn depends_on(&self) -> Vec<String> {
|
|
118
124
|
vec![]
|
|
119
125
|
}
|
|
@@ -260,7 +260,7 @@ mod tests {
|
|
|
260
260
|
StatusCode::BAD_REQUEST,
|
|
261
261
|
"validation_error",
|
|
262
262
|
"Invalid email format",
|
|
263
|
-
details
|
|
263
|
+
details,
|
|
264
264
|
);
|
|
265
265
|
assert_eq!(status, StatusCode::BAD_REQUEST);
|
|
266
266
|
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
@@ -273,7 +273,7 @@ mod tests {
|
|
|
273
273
|
#[test]
|
|
274
274
|
fn test_from_structured_error() {
|
|
275
275
|
let error = StructuredError::simple("test_error", "Something went wrong");
|
|
276
|
-
let (status, body) = ErrorResponseBuilder::from_structured_error(error);
|
|
276
|
+
let (status, body) = ErrorResponseBuilder::from_structured_error(&error);
|
|
277
277
|
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
|
|
278
278
|
let parsed: Value = serde_json::from_str(&body).unwrap();
|
|
279
279
|
assert_eq!(parsed["code"], "test_error");
|
|
@@ -287,7 +287,7 @@ mod tests {
|
|
|
287
287
|
error_type: "missing".to_string(),
|
|
288
288
|
loc: vec!["body".to_string(), "username".to_string()],
|
|
289
289
|
msg: "Field required".to_string(),
|
|
290
|
-
input: Value::String(
|
|
290
|
+
input: Value::String(String::new()),
|
|
291
291
|
ctx: None,
|
|
292
292
|
}],
|
|
293
293
|
};
|
|
@@ -33,7 +33,9 @@ pub enum HandlerError {
|
|
|
33
33
|
|
|
34
34
|
impl From<ValidationError> for HandlerError {
|
|
35
35
|
fn from(err: ValidationError) -> Self {
|
|
36
|
-
|
|
36
|
+
// PERFORMANCE: Avoid format! allocation for debug representation.
|
|
37
|
+
// Most callers just need the error type, not the full debug output.
|
|
38
|
+
Self::Validation(err.to_string())
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -125,6 +127,9 @@ impl<L: LanguageHandler + 'static> Handler for HandlerExecutor<L> {
|
|
|
125
127
|
return Err(ErrorResponseBuilder::validation_error(&validation_err));
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
// PERFORMANCE: Avoid format! allocations in the hot path. ErrorResponseBuilder
|
|
131
|
+
// can accept &dyn Display or construct error messages directly, reducing
|
|
132
|
+
// string allocation overhead in typical error handling paths.
|
|
128
133
|
let input = self
|
|
129
134
|
.language_handler
|
|
130
135
|
.prepare_request(&request_data)
|
|
@@ -181,10 +186,10 @@ mod tests {
|
|
|
181
186
|
let request = Request::builder().body(Body::empty()).unwrap();
|
|
182
187
|
let request_data = RequestData {
|
|
183
188
|
path_params: Arc::new(std::collections::HashMap::new()),
|
|
184
|
-
query_params: json!({}),
|
|
189
|
+
query_params: Arc::new(json!({})),
|
|
185
190
|
validated_params: None,
|
|
186
191
|
raw_query_params: Arc::new(std::collections::HashMap::new()),
|
|
187
|
-
body: json!({}),
|
|
192
|
+
body: Arc::new(json!({})),
|
|
188
193
|
raw_body: None,
|
|
189
194
|
headers: Arc::new(std::collections::HashMap::new()),
|
|
190
195
|
cookies: Arc::new(std::collections::HashMap::new()),
|
|
@@ -205,10 +210,10 @@ mod tests {
|
|
|
205
210
|
let request = Request::builder().body(Body::empty()).unwrap();
|
|
206
211
|
let request_data = RequestData {
|
|
207
212
|
path_params: Arc::new(std::collections::HashMap::new()),
|
|
208
|
-
query_params: json!({}),
|
|
213
|
+
query_params: Arc::new(json!({})),
|
|
209
214
|
validated_params: None,
|
|
210
215
|
raw_query_params: Arc::new(std::collections::HashMap::new()),
|
|
211
|
-
body: json!({}),
|
|
216
|
+
body: Arc::new(json!({})),
|
|
212
217
|
raw_body: None,
|
|
213
218
|
headers: Arc::new(std::collections::HashMap::new()),
|
|
214
219
|
cookies: Arc::new(std::collections::HashMap::new()),
|