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.
Files changed (106) 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 +2 -2
  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 +3 -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 +2 -2
  13. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +4 -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 +3 -3
  16. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +10 -5
  17. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
  18. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
  19. data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
  20. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +11 -11
  21. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +9 -37
  22. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +436 -3
  23. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
  24. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +4 -4
  25. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
  26. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
  27. data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
  28. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
  29. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
  30. data/vendor/crates/spikard-core/Cargo.toml +3 -3
  31. data/vendor/crates/spikard-core/src/di/container.rs +1 -1
  32. data/vendor/crates/spikard-core/src/di/factory.rs +2 -2
  33. data/vendor/crates/spikard-core/src/di/resolved.rs +2 -2
  34. data/vendor/crates/spikard-core/src/di/value.rs +1 -1
  35. data/vendor/crates/spikard-core/src/http.rs +75 -0
  36. data/vendor/crates/spikard-core/src/lifecycle.rs +43 -43
  37. data/vendor/crates/spikard-core/src/parameters.rs +14 -19
  38. data/vendor/crates/spikard-core/src/problem.rs +1 -1
  39. data/vendor/crates/spikard-core/src/request_data.rs +7 -16
  40. data/vendor/crates/spikard-core/src/router.rs +6 -0
  41. data/vendor/crates/spikard-core/src/schema_registry.rs +2 -3
  42. data/vendor/crates/spikard-core/src/type_hints.rs +3 -2
  43. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +1 -1
  44. data/vendor/crates/spikard-core/src/validation/mod.rs +1 -1
  45. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
  46. data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
  47. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
  48. data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
  49. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
  50. data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
  51. data/vendor/crates/spikard-http/Cargo.toml +4 -2
  52. data/vendor/crates/spikard-http/src/cors.rs +32 -11
  53. data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
  54. data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
  55. data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
  56. data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
  57. data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
  58. data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
  59. data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
  60. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
  61. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
  62. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
  63. data/vendor/crates/spikard-http/src/lib.rs +1 -1
  64. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
  65. data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
  66. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
  67. data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
  68. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
  69. data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
  70. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
  71. data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
  72. data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
  73. data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
  74. data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
  75. data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
  76. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
  77. data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
  78. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
  79. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
  80. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
  81. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
  82. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
  83. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
  84. data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
  85. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
  86. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
  87. data/vendor/crates/spikard-rb/Cargo.toml +3 -1
  88. data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
  89. data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
  90. data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
  91. data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
  92. data/vendor/crates/spikard-rb/src/handler.rs +169 -91
  93. data/vendor/crates/spikard-rb/src/lib.rs +444 -62
  94. data/vendor/crates/spikard-rb/src/lifecycle.rs +29 -1
  95. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
  96. data/vendor/crates/spikard-rb/src/request.rs +117 -20
  97. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
  98. data/vendor/crates/spikard-rb/src/server.rs +23 -14
  99. data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
  100. data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
  101. data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
  102. data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
  103. data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
  104. metadata +14 -4
  105. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
  106. 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.3"
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 vendored crates for packaged gem distribution
16
+ # Use crates from the workspace root
17
17
  spikard_rb_core = { package = "spikard-rb", path = "../../vendor/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.3'
4
+ VERSION = '0.10.2'
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,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-bindings-shared"
3
- version = "0.8.3"
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", 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 ===");
@@ -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(5242880));
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).unwrap();
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| TestType { value: 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.clone());
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.clone());
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.clone());
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>, 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
  }
@@ -260,7 +260,7 @@ mod tests {
260
260
  StatusCode::BAD_REQUEST,
261
261
  "validation_error",
262
262
  "Invalid email format",
263
- details.clone(),
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("".to_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
- Self::Validation(format!("{err:?}"))
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()),