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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -10
  3. data/ext/spikard_rb/Cargo.lock +234 -162
  4. data/ext/spikard_rb/Cargo.toml +3 -3
  5. data/ext/spikard_rb/extconf.rb +4 -3
  6. data/lib/spikard/config.rb +88 -12
  7. data/lib/spikard/testing.rb +3 -1
  8. data/lib/spikard/version.rb +1 -1
  9. data/lib/spikard.rb +11 -0
  10. data/vendor/crates/spikard-bindings-shared/Cargo.toml +11 -6
  11. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
  12. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +63 -25
  13. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +20 -4
  14. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
  15. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +25 -22
  16. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +14 -12
  17. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +24 -10
  18. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
  19. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
  20. data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
  21. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +17 -11
  22. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +51 -73
  23. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +442 -4
  24. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
  25. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +22 -10
  26. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +28 -10
  27. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
  28. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
  29. data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
  30. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
  31. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
  32. data/vendor/crates/spikard-core/Cargo.toml +11 -3
  33. data/vendor/crates/spikard-core/src/bindings/response.rs +6 -9
  34. data/vendor/crates/spikard-core/src/debug.rs +2 -2
  35. data/vendor/crates/spikard-core/src/di/container.rs +2 -2
  36. data/vendor/crates/spikard-core/src/di/error.rs +1 -1
  37. data/vendor/crates/spikard-core/src/di/factory.rs +9 -5
  38. data/vendor/crates/spikard-core/src/di/graph.rs +1 -0
  39. data/vendor/crates/spikard-core/src/di/resolved.rs +25 -2
  40. data/vendor/crates/spikard-core/src/di/value.rs +2 -1
  41. data/vendor/crates/spikard-core/src/errors.rs +3 -0
  42. data/vendor/crates/spikard-core/src/http.rs +94 -18
  43. data/vendor/crates/spikard-core/src/lifecycle.rs +85 -61
  44. data/vendor/crates/spikard-core/src/parameters.rs +75 -54
  45. data/vendor/crates/spikard-core/src/problem.rs +19 -5
  46. data/vendor/crates/spikard-core/src/request_data.rs +16 -24
  47. data/vendor/crates/spikard-core/src/router.rs +26 -6
  48. data/vendor/crates/spikard-core/src/schema_registry.rs +25 -11
  49. data/vendor/crates/spikard-core/src/type_hints.rs +14 -7
  50. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +30 -16
  51. data/vendor/crates/spikard-core/src/validation/mod.rs +46 -33
  52. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
  53. data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
  54. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
  55. data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
  56. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
  57. data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
  58. data/vendor/crates/spikard-http/Cargo.toml +11 -2
  59. data/vendor/crates/spikard-http/src/cors.rs +32 -11
  60. data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
  61. data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
  62. data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
  63. data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
  64. data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
  65. data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
  66. data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
  67. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
  68. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
  69. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
  70. data/vendor/crates/spikard-http/src/lib.rs +1 -1
  71. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
  72. data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
  73. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
  74. data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
  75. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
  76. data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
  77. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
  78. data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
  79. data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
  80. data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
  81. data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
  82. data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
  83. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
  84. data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
  85. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
  86. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
  87. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
  88. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
  89. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
  90. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
  91. data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
  92. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
  93. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
  94. data/vendor/crates/spikard-rb/Cargo.toml +11 -1
  95. data/vendor/crates/spikard-rb/build.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
  97. data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
  98. data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
  99. data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
  100. data/vendor/crates/spikard-rb/src/handler.rs +169 -91
  101. data/vendor/crates/spikard-rb/src/lib.rs +502 -62
  102. data/vendor/crates/spikard-rb/src/lifecycle.rs +31 -3
  103. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
  104. data/vendor/crates/spikard-rb/src/request.rs +117 -20
  105. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
  106. data/vendor/crates/spikard-rb/src/server.rs +23 -14
  107. data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
  108. data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
  109. data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
  110. data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
  111. data/vendor/crates/spikard-rb-macros/Cargo.toml +9 -1
  112. data/vendor/crates/spikard-rb-macros/src/lib.rs +4 -5
  113. metadata +14 -4
  114. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
  115. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +0 -2
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-rb-ext"
3
- version = "0.8.2"
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 vendored crates for packaged gem distribution
17
- spikard_rb_core = { package = "spikard-rb", path = "../../vendor/crates/spikard-rb" }
16
+ # Use crates from the workspace root
17
+ spikard_rb_core = { package = "spikard-rb", path = "../../../../crates/spikard-rb" }
@@ -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
- # Only use --locked in development to prevent lockfile updates
11
- # Release builds need to update Cargo.lock after version bumps
12
- config.extra_cargo_args = ['--locked'] unless default_profile == 'release'
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
@@ -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
@@ -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
- Spikard::Native::TestClient.new(routes_json, handlers, config, ws_handlers, sse_producers, dependencies)
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spikard
4
- VERSION = '0.8.2'
4
+ VERSION = '0.10.1'
5
5
  end
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.8.2"
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", e),
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", e),
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", e),
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", e),
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", e),
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, JavaScript Object, etc.).
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
- .get_u16("port")
78
- .or_else(|| source.get_u32("port").map(|p| p as u16))
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
- Ok(HashMap::new())
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(5242880));
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).unwrap();
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| TestType { value: 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.clone());
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.clone());
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.clone());
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>, Opaque<Value>, etc.)
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> that can be downcast to the concrete type
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>, ThreadsafeFunction, etc.)
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
  }