spikard 0.8.2 → 0.10.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 +19 -10
- data/ext/spikard_rb/Cargo.lock +234 -162
- data/ext/spikard_rb/Cargo.toml +3 -3
- 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 +11 -6
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +63 -25
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +20 -4
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +25 -22
- data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +14 -12
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +24 -10
- 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 +17 -11
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +51 -73
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +442 -4
- data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +22 -10
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +28 -10
- 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 +11 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +6 -9
- data/vendor/crates/spikard-core/src/debug.rs +2 -2
- data/vendor/crates/spikard-core/src/di/container.rs +2 -2
- data/vendor/crates/spikard-core/src/di/error.rs +1 -1
- data/vendor/crates/spikard-core/src/di/factory.rs +9 -5
- data/vendor/crates/spikard-core/src/di/graph.rs +1 -0
- data/vendor/crates/spikard-core/src/di/resolved.rs +25 -2
- data/vendor/crates/spikard-core/src/di/value.rs +2 -1
- data/vendor/crates/spikard-core/src/errors.rs +3 -0
- data/vendor/crates/spikard-core/src/http.rs +94 -18
- data/vendor/crates/spikard-core/src/lifecycle.rs +85 -61
- data/vendor/crates/spikard-core/src/parameters.rs +75 -54
- data/vendor/crates/spikard-core/src/problem.rs +19 -5
- data/vendor/crates/spikard-core/src/request_data.rs +16 -24
- data/vendor/crates/spikard-core/src/router.rs +26 -6
- data/vendor/crates/spikard-core/src/schema_registry.rs +25 -11
- data/vendor/crates/spikard-core/src/type_hints.rs +14 -7
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +30 -16
- data/vendor/crates/spikard-core/src/validation/mod.rs +46 -33
- 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 +11 -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 +11 -1
- data/vendor/crates/spikard-rb/build.rs +1 -0
- 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 +502 -62
- data/vendor/crates/spikard-rb/src/lifecycle.rs +31 -3
- 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 +9 -1
- data/vendor/crates/spikard-rb-macros/src/lib.rs +4 -5
- 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.1"
|
|
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
|
|
17
|
-
spikard_rb_core = { package = "spikard-rb", path = "
|
|
16
|
+
# Use crates from the workspace root
|
|
17
|
+
spikard_rb_core = { package = "spikard-rb", path = "../../../../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,15 +1,24 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "spikard-bindings-shared"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.1"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
7
7
|
repository = "https://github.com/Goldziher/spikard"
|
|
8
8
|
homepage = "https://github.com/Goldziher/spikard"
|
|
9
9
|
|
|
10
|
+
[lints.rust]
|
|
11
|
+
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] }
|
|
12
|
+
|
|
13
|
+
[lints.clippy]
|
|
14
|
+
all = { level = "deny", priority = 0 }
|
|
15
|
+
pedantic = { level = "deny", priority = 0 }
|
|
16
|
+
nursery = { level = "deny", priority = 0 }
|
|
17
|
+
|
|
10
18
|
[dependencies]
|
|
11
19
|
serde = { version = "1.0", features = ["derive"] }
|
|
12
20
|
serde_json = "1.0"
|
|
21
|
+
simd-json = "0.17"
|
|
13
22
|
axum = { version = "0.8", features = ["multipart", "ws"] }
|
|
14
23
|
tokio = { version = "1", features = ["full"] }
|
|
15
24
|
thiserror = "2.0"
|
|
@@ -19,6 +28,7 @@ tracing = "0.1"
|
|
|
19
28
|
http = "1.4"
|
|
20
29
|
http-body-util = "0.1"
|
|
21
30
|
tonic = "0.14"
|
|
31
|
+
bytes = "1.11"
|
|
22
32
|
|
|
23
33
|
[features]
|
|
24
34
|
default = []
|
|
@@ -26,7 +36,6 @@ python-support = ["pyo3", "pyo3-async-runtimes"]
|
|
|
26
36
|
node-support = ["napi", "napi-derive"]
|
|
27
37
|
ruby-support = ["magnus"]
|
|
28
38
|
php-support = ["ext-php-rs"]
|
|
29
|
-
wasm-support = ["wasm-bindgen"]
|
|
30
39
|
|
|
31
40
|
[dependencies.pyo3]
|
|
32
41
|
version = "0.27"
|
|
@@ -56,9 +65,5 @@ optional = true
|
|
|
56
65
|
version = "0.15"
|
|
57
66
|
optional = true
|
|
58
67
|
|
|
59
|
-
[dependencies.wasm-bindgen]
|
|
60
|
-
version = "0.2"
|
|
61
|
-
optional = true
|
|
62
|
-
|
|
63
68
|
[dev-dependencies]
|
|
64
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 ===");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//! Configuration extraction trait and implementation for language bindings
|
|
2
2
|
//!
|
|
3
|
-
//! This module provides a trait-based abstraction for extracting ServerConfig and related
|
|
3
|
+
//! This module provides a trait-based abstraction for extracting `ServerConfig` and related
|
|
4
4
|
//! configuration structs from language-specific objects (Python dicts, JavaScript objects, etc.)
|
|
5
5
|
//! without duplicating extraction logic across bindings.
|
|
6
6
|
//!
|
|
@@ -17,7 +17,7 @@ use std::collections::HashMap;
|
|
|
17
17
|
/// Trait for reading configuration from language-specific objects
|
|
18
18
|
///
|
|
19
19
|
/// Bindings implement this trait to provide unified access to configuration values
|
|
20
|
-
/// regardless of the language-specific representation (PyDict
|
|
20
|
+
/// regardless of the language-specific representation (`PyDict`, JavaScript Object, etc.).
|
|
21
21
|
pub trait ConfigSource {
|
|
22
22
|
/// Get a boolean value from the source
|
|
23
23
|
fn get_bool(&self, key: &str) -> Option<bool>;
|
|
@@ -34,7 +34,7 @@ pub trait ConfigSource {
|
|
|
34
34
|
/// Get a vector of strings from the source
|
|
35
35
|
fn get_vec_string(&self, key: &str) -> Option<Vec<String>>;
|
|
36
36
|
|
|
37
|
-
/// Get a nested ConfigSource for nested objects
|
|
37
|
+
/// Get a nested `ConfigSource` for nested objects
|
|
38
38
|
fn get_nested(&self, key: &str) -> Option<Box<dyn ConfigSource + '_>>;
|
|
39
39
|
|
|
40
40
|
/// Check if a key exists in the source
|
|
@@ -61,11 +61,15 @@ pub trait ConfigSource {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/// Configuration extractor that works with any ConfigSource
|
|
64
|
+
/// Configuration extractor that works with any `ConfigSource`
|
|
65
65
|
pub struct ConfigExtractor;
|
|
66
66
|
|
|
67
67
|
impl ConfigExtractor {
|
|
68
|
-
/// Extract a complete ServerConfig from a ConfigSource
|
|
68
|
+
/// Extract a complete `ServerConfig` from a `ConfigSource`
|
|
69
|
+
///
|
|
70
|
+
/// # Errors
|
|
71
|
+
///
|
|
72
|
+
/// Returns an error if required configuration fields are invalid or missing.
|
|
69
73
|
pub fn extract_server_config(source: &dyn ConfigSource) -> Result<ServerConfig, String> {
|
|
70
74
|
let mut config = ServerConfig::default();
|
|
71
75
|
|
|
@@ -73,10 +77,14 @@ impl ConfigExtractor {
|
|
|
73
77
|
config.host = host;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
if let Some(port) = source
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
if let Some(port) = source.get_u16("port").or_else(|| {
|
|
81
|
+
source.get_u32("port").map(|p| {
|
|
82
|
+
#[allow(clippy::cast_possible_truncation)]
|
|
83
|
+
{
|
|
84
|
+
p as u16
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}) {
|
|
80
88
|
config.port = port;
|
|
81
89
|
}
|
|
82
90
|
|
|
@@ -144,7 +152,11 @@ impl ConfigExtractor {
|
|
|
144
152
|
Ok(config)
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
/// Extract CompressionConfig from a ConfigSource
|
|
155
|
+
/// Extract `CompressionConfig` from a `ConfigSource`
|
|
156
|
+
///
|
|
157
|
+
/// # Errors
|
|
158
|
+
///
|
|
159
|
+
/// Returns an error if required configuration fields are invalid.
|
|
148
160
|
pub fn extract_compression_config(source: &dyn ConfigSource) -> Result<CompressionConfig, String> {
|
|
149
161
|
let gzip = source.get_bool("gzip").unwrap_or(true);
|
|
150
162
|
let brotli = source.get_bool("brotli").unwrap_or(true);
|
|
@@ -162,7 +174,11 @@ impl ConfigExtractor {
|
|
|
162
174
|
})
|
|
163
175
|
}
|
|
164
176
|
|
|
165
|
-
/// Extract RateLimitConfig from a ConfigSource
|
|
177
|
+
/// Extract `RateLimitConfig` from a `ConfigSource`
|
|
178
|
+
///
|
|
179
|
+
/// # Errors
|
|
180
|
+
///
|
|
181
|
+
/// Returns an error if required fields `per_second` or `burst` are missing.
|
|
166
182
|
pub fn extract_rate_limit_config(source: &dyn ConfigSource) -> Result<RateLimitConfig, String> {
|
|
167
183
|
let per_second = source.get_u64("per_second").ok_or("Rate limit requires 'per_second'")?;
|
|
168
184
|
|
|
@@ -177,7 +193,11 @@ impl ConfigExtractor {
|
|
|
177
193
|
})
|
|
178
194
|
}
|
|
179
195
|
|
|
180
|
-
/// Extract JwtConfig from a ConfigSource
|
|
196
|
+
/// Extract `JwtConfig` from a `ConfigSource`
|
|
197
|
+
///
|
|
198
|
+
/// # Errors
|
|
199
|
+
///
|
|
200
|
+
/// Returns an error if the required `secret` field is missing.
|
|
181
201
|
pub fn extract_jwt_config(source: &dyn ConfigSource) -> Result<JwtConfig, String> {
|
|
182
202
|
let secret = source.get_string("secret").ok_or("JWT auth requires 'secret'")?;
|
|
183
203
|
|
|
@@ -198,7 +218,11 @@ impl ConfigExtractor {
|
|
|
198
218
|
})
|
|
199
219
|
}
|
|
200
220
|
|
|
201
|
-
/// Extract ApiKeyConfig from a ConfigSource
|
|
221
|
+
/// Extract `ApiKeyConfig` from a `ConfigSource`
|
|
222
|
+
///
|
|
223
|
+
/// # Errors
|
|
224
|
+
///
|
|
225
|
+
/// Returns an error if the required `keys` field is missing.
|
|
202
226
|
pub fn extract_api_key_config(source: &dyn ConfigSource) -> Result<ApiKeyConfig, String> {
|
|
203
227
|
let keys = source
|
|
204
228
|
.get_vec_string("keys")
|
|
@@ -211,7 +235,11 @@ impl ConfigExtractor {
|
|
|
211
235
|
Ok(ApiKeyConfig { keys, header_name })
|
|
212
236
|
}
|
|
213
237
|
|
|
214
|
-
/// Extract static files configuration list from a ConfigSource
|
|
238
|
+
/// Extract static files configuration list from a `ConfigSource`
|
|
239
|
+
///
|
|
240
|
+
/// # Errors
|
|
241
|
+
///
|
|
242
|
+
/// Returns an error if array elements are invalid or missing required fields.
|
|
215
243
|
pub fn extract_static_files_config(source: &dyn ConfigSource) -> Result<Vec<StaticFilesConfig>, String> {
|
|
216
244
|
let length = source.get_array_length("static_files").unwrap_or(0);
|
|
217
245
|
if length == 0 {
|
|
@@ -247,7 +275,11 @@ impl ConfigExtractor {
|
|
|
247
275
|
Ok(configs)
|
|
248
276
|
}
|
|
249
277
|
|
|
250
|
-
/// Extract OpenApiConfig from a ConfigSource
|
|
278
|
+
/// Extract `OpenApiConfig` from a `ConfigSource`
|
|
279
|
+
///
|
|
280
|
+
/// # Errors
|
|
281
|
+
///
|
|
282
|
+
/// Returns an error if required configuration fields are invalid.
|
|
251
283
|
pub fn extract_openapi_config(source: &dyn ConfigSource) -> Result<OpenApiConfig, String> {
|
|
252
284
|
let enabled = source.get_bool("enabled").unwrap_or(false);
|
|
253
285
|
let title = source.get_string("title").unwrap_or_else(|| "API".to_string());
|
|
@@ -279,7 +311,7 @@ impl ConfigExtractor {
|
|
|
279
311
|
|
|
280
312
|
let servers = Self::extract_servers_config(source)?;
|
|
281
313
|
|
|
282
|
-
let security_schemes = Self::extract_security_schemes_config(source)
|
|
314
|
+
let security_schemes = Self::extract_security_schemes_config(source);
|
|
283
315
|
|
|
284
316
|
Ok(OpenApiConfig {
|
|
285
317
|
enabled,
|
|
@@ -296,7 +328,11 @@ impl ConfigExtractor {
|
|
|
296
328
|
})
|
|
297
329
|
}
|
|
298
330
|
|
|
299
|
-
/// Extract servers list from OpenAPI config
|
|
331
|
+
/// Extract servers list from `OpenAPI` config
|
|
332
|
+
///
|
|
333
|
+
/// # Errors
|
|
334
|
+
///
|
|
335
|
+
/// Returns an error if array elements are invalid or missing.
|
|
300
336
|
fn extract_servers_config(source: &dyn ConfigSource) -> Result<Vec<ServerInfo>, String> {
|
|
301
337
|
let length = source.get_array_length("servers").unwrap_or(0);
|
|
302
338
|
if length == 0 {
|
|
@@ -319,15 +355,17 @@ impl ConfigExtractor {
|
|
|
319
355
|
Ok(servers)
|
|
320
356
|
}
|
|
321
357
|
|
|
322
|
-
/// Extract security schemes from OpenAPI config
|
|
323
|
-
fn extract_security_schemes_config(
|
|
324
|
-
_source: &dyn ConfigSource,
|
|
325
|
-
) -> Result<HashMap<String, SecuritySchemeInfo>, String> {
|
|
358
|
+
/// Extract security schemes from `OpenAPI` config
|
|
359
|
+
fn extract_security_schemes_config(_source: &dyn ConfigSource) -> HashMap<String, SecuritySchemeInfo> {
|
|
326
360
|
// TODO: Implement when bindings support iterating HashMap-like structures
|
|
327
|
-
|
|
361
|
+
HashMap::new()
|
|
328
362
|
}
|
|
329
363
|
|
|
330
|
-
/// Extract JsonRpcConfig from a ConfigSource
|
|
364
|
+
/// Extract `JsonRpcConfig` from a `ConfigSource`
|
|
365
|
+
///
|
|
366
|
+
/// # Errors
|
|
367
|
+
///
|
|
368
|
+
/// Returns an error if required configuration fields are invalid.
|
|
331
369
|
pub fn extract_jsonrpc_config(source: &dyn ConfigSource) -> Result<JsonRpcConfig, String> {
|
|
332
370
|
let enabled = source.get_bool("enabled").unwrap_or(true);
|
|
333
371
|
let endpoint_path = source.get_string("endpoint_path").unwrap_or_else(|| "/rpc".to_string());
|
|
@@ -549,7 +587,7 @@ mod tests {
|
|
|
549
587
|
assert_eq!(config.port, 3000);
|
|
550
588
|
assert_eq!(config.workers, 4);
|
|
551
589
|
assert!(!config.enable_request_id);
|
|
552
|
-
assert_eq!(config.max_body_size, Some(
|
|
590
|
+
assert_eq!(config.max_body_size, Some(5_242_880));
|
|
553
591
|
assert_eq!(config.request_timeout, Some(60));
|
|
554
592
|
assert!(!config.graceful_shutdown);
|
|
555
593
|
assert_eq!(config.shutdown_timeout, 10);
|
|
@@ -582,7 +620,7 @@ mod tests {
|
|
|
582
620
|
fn test_security_schemes_config_empty() {
|
|
583
621
|
let source = MockConfigSource::new();
|
|
584
622
|
|
|
585
|
-
let schemes = ConfigExtractor::extract_security_schemes_config(&source)
|
|
623
|
+
let schemes = ConfigExtractor::extract_security_schemes_config(&source);
|
|
586
624
|
assert_eq!(schemes.len(), 0);
|
|
587
625
|
}
|
|
588
626
|
|
|
@@ -8,6 +8,10 @@ pub trait FromLanguage: Sized {
|
|
|
8
8
|
type Error: std::fmt::Display;
|
|
9
9
|
|
|
10
10
|
/// Convert from a language-specific value
|
|
11
|
+
///
|
|
12
|
+
/// # Errors
|
|
13
|
+
///
|
|
14
|
+
/// Returns an error if the value cannot be converted to the expected type.
|
|
11
15
|
fn from_any(value: &(dyn Any + Send + Sync)) -> Result<Self, Self::Error>;
|
|
12
16
|
}
|
|
13
17
|
|
|
@@ -17,6 +21,10 @@ pub trait ToLanguage {
|
|
|
17
21
|
type Error: std::fmt::Display;
|
|
18
22
|
|
|
19
23
|
/// Convert to a language-specific value
|
|
24
|
+
///
|
|
25
|
+
/// # Errors
|
|
26
|
+
///
|
|
27
|
+
/// Returns an error if the conversion fails.
|
|
20
28
|
fn to_any(&self) -> Result<Box<dyn Any + Send + Sync>, Self::Error>;
|
|
21
29
|
}
|
|
22
30
|
|
|
@@ -26,9 +34,17 @@ pub trait JsonConvertible: Sized {
|
|
|
26
34
|
type Error: std::fmt::Display;
|
|
27
35
|
|
|
28
36
|
/// Convert from a JSON value
|
|
37
|
+
///
|
|
38
|
+
/// # Errors
|
|
39
|
+
///
|
|
40
|
+
/// Returns an error if the JSON value is not valid for the target type.
|
|
29
41
|
fn from_json(value: serde_json::Value) -> Result<Self, Self::Error>;
|
|
30
42
|
|
|
31
43
|
/// Convert to a JSON value
|
|
44
|
+
///
|
|
45
|
+
/// # Errors
|
|
46
|
+
///
|
|
47
|
+
/// Returns an error if the conversion fails.
|
|
32
48
|
fn to_json(&self) -> Result<serde_json::Value, Self::Error>;
|
|
33
49
|
}
|
|
34
50
|
|
|
@@ -71,7 +87,7 @@ mod tests {
|
|
|
71
87
|
fn from_any(value: &(dyn Any + Send + Sync)) -> Result<Self, Self::Error> {
|
|
72
88
|
value
|
|
73
89
|
.downcast_ref::<i32>()
|
|
74
|
-
.map(|&v|
|
|
90
|
+
.map(|&v| Self { value: v })
|
|
75
91
|
.ok_or_else(|| "Invalid type".to_string())
|
|
76
92
|
}
|
|
77
93
|
}
|
|
@@ -153,7 +169,7 @@ mod tests {
|
|
|
153
169
|
#[test]
|
|
154
170
|
fn test_json_null_conversion() {
|
|
155
171
|
let null_value = serde_json::Value::Null;
|
|
156
|
-
let result = serde_json::Value::from_json(null_value
|
|
172
|
+
let result = serde_json::Value::from_json(null_value);
|
|
157
173
|
assert!(result.is_ok());
|
|
158
174
|
assert!(result.unwrap().is_null());
|
|
159
175
|
}
|
|
@@ -161,7 +177,7 @@ mod tests {
|
|
|
161
177
|
#[test]
|
|
162
178
|
fn test_json_array_conversion() {
|
|
163
179
|
let array = json!([1, 2, 3, 4, 5]);
|
|
164
|
-
let result = serde_json::Value::from_json(array
|
|
180
|
+
let result = serde_json::Value::from_json(array);
|
|
165
181
|
assert!(result.is_ok());
|
|
166
182
|
let converted = result.unwrap();
|
|
167
183
|
assert!(converted.is_array());
|
|
@@ -180,7 +196,7 @@ mod tests {
|
|
|
180
196
|
}
|
|
181
197
|
});
|
|
182
198
|
|
|
183
|
-
let result = serde_json::Value::from_json(nested
|
|
199
|
+
let result = serde_json::Value::from_json(nested);
|
|
184
200
|
assert!(result.is_ok());
|
|
185
201
|
let converted = result.unwrap();
|
|
186
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
|
}
|