spikard 0.3.6 → 0.5.0

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -6
  3. data/ext/spikard_rb/Cargo.toml +2 -2
  4. data/lib/spikard/app.rb +33 -14
  5. data/lib/spikard/testing.rb +47 -12
  6. data/lib/spikard/version.rb +1 -1
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  8. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  9. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  10. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  11. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  12. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  13. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  14. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  15. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  16. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  17. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  18. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  19. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  20. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  21. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  22. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  23. data/vendor/crates/spikard-core/Cargo.toml +4 -4
  24. data/vendor/crates/spikard-core/src/debug.rs +64 -0
  25. data/vendor/crates/spikard-core/src/di/container.rs +3 -27
  26. data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
  27. data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
  28. data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
  29. data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
  30. data/vendor/crates/spikard-core/src/di/value.rs +2 -4
  31. data/vendor/crates/spikard-core/src/errors.rs +30 -0
  32. data/vendor/crates/spikard-core/src/http.rs +262 -0
  33. data/vendor/crates/spikard-core/src/lib.rs +1 -1
  34. data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
  35. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  36. data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
  37. data/vendor/crates/spikard-core/src/problem.rs +34 -0
  38. data/vendor/crates/spikard-core/src/request_data.rs +966 -1
  39. data/vendor/crates/spikard-core/src/router.rs +263 -2
  40. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  41. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
  42. data/vendor/crates/spikard-http/Cargo.toml +12 -16
  43. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  44. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  45. data/vendor/crates/spikard-http/src/auth.rs +65 -16
  46. data/vendor/crates/spikard-http/src/background.rs +1614 -3
  47. data/vendor/crates/spikard-http/src/cors.rs +515 -0
  48. data/vendor/crates/spikard-http/src/debug.rs +65 -0
  49. data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
  50. data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
  51. data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
  52. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
  53. data/vendor/crates/spikard-http/src/lib.rs +33 -28
  54. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
  55. data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
  56. data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
  57. data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
  58. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
  59. data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
  60. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
  61. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
  62. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
  63. data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
  64. data/vendor/crates/spikard-http/src/response.rs +321 -0
  65. data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
  66. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
  67. data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
  68. data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
  69. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  70. data/vendor/crates/spikard-http/src/sse.rs +983 -21
  71. data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
  72. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
  73. data/vendor/crates/spikard-http/src/testing.rs +7 -7
  74. data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
  75. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  76. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  77. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  78. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  79. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  80. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  81. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  82. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  83. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  84. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  85. data/vendor/crates/spikard-rb/Cargo.toml +10 -4
  86. data/vendor/crates/spikard-rb/build.rs +196 -5
  87. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  88. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
  89. data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
  90. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  91. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
  92. data/vendor/crates/spikard-rb/src/handler.rs +100 -107
  93. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  94. data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  97. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  98. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  99. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  100. data/vendor/crates/spikard-rb/src/server.rs +47 -22
  101. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
  102. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  103. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  104. data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
  105. metadata +46 -13
  106. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  107. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  108. data/vendor/crates/spikard-http/src/router.rs +0 -1
  109. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  110. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  111. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  112. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  113. /data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5fecf669ec67e8c861f1109e55d813f3daf43cb09596cee0f9ac0ee56514c5a
4
- data.tar.gz: a3ffe8c8013a24dbaa4ba89310ec1ea0a0aec97eb2b7d95d2bfa392eb86e65fc
3
+ metadata.gz: ab9d6a7c6017e06cb3fcc9eb7396ae3771e8e4176907d0d3a5b5af0597823310
4
+ data.tar.gz: e37b6cb0b218622a2abbfdb58f2a20c1bfa50cea53af005e5802573bdfe51f52
5
5
  SHA512:
6
- metadata.gz: b3d38a4d2da68aaa53953927f6c7c450abea613e8a24d68a866d9f8dcbd4a15ad28141cb5852728aea75fa5b8f50ca729529d399868924f1f33a3357cf7545a3
7
- data.tar.gz: 327c499797f33123a98b4f096a8847dd63f99ff7c4d729ddb0afd6b1d2ea617b3f431dff899b61aa54cb4eb833c512bebf5535f214df23e06da2ab4c6c09189a
6
+ metadata.gz: 5af5495f8b41a896fbc792914b319902f61ec1807db95e648bd41afa141f6a0021ca899311bf4d4d8fd17217911ed05f77e3238e756a4a332814382d259fa944
7
+ data.tar.gz: 9664886689994eee085ef08e03a345e598d8529de7a6df27b96dd8626923c6f5bbff442b14730747e9ca11d4bdf9a525b77a83a762b8d004ba60ae18425b259d
data/README.md CHANGED
@@ -4,12 +4,8 @@
4
4
  [![Gem Version](https://img.shields.io/gem/v/spikard.svg)](https://rubygems.org/gems/spikard)
5
5
  [![Gem Downloads](https://img.shields.io/gem/dt/spikard.svg)](https://rubygems.org/gems/spikard)
6
6
  [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.2-red.svg)](https://www.ruby-lang.org/)
7
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
7
  [![codecov](https://codecov.io/gh/Goldziher/spikard/graph/badge.svg?token=H4ZXDZ4A69)](https://codecov.io/gh/Goldziher/spikard)
9
- [![PyPI](https://img.shields.io/pypi/v/spikard.svg)](https://pypi.org/project/spikard/)
10
- [![npm](https://img.shields.io/npm/v/spikard.svg)](https://www.npmjs.com/package/spikard)
11
- [![Crates.io](https://img.shields.io/crates/v/spikard.svg)](https://crates.io/crates/spikard)
12
- [![Packagist](https://img.shields.io/packagist/v/spikard/spikard.svg)](https://packagist.org/packages/spikard/spikard)
8
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
13
9
 
14
10
  High-performance Ruby web framework with a Rust core. Build REST APIs with Sinatra-style routing and zero-overhead async handlers backed by Axum and Tower-HTTP.
15
11
 
@@ -245,7 +241,7 @@ app.provide("db_pool", depends_on: ["config"], singleton: true) do |config:|
245
241
  { url: config["db_url"], driver: "pool" }
246
242
  end
247
243
 
248
- app.get "/stats" do |_params, _query, _body, config:, db_pool:|
244
+ app.get "/stats" do |request, config:, db_pool:|
249
245
  { db: db_pool[:url], env: config["db_url"] }
250
246
  end
251
247
  ```
@@ -617,6 +613,25 @@ Ruby bindings use:
617
613
  - Idiomatic Ruby blocks and procs
618
614
  - GC-safe handler storage
619
615
 
616
+ ### CI Benchmarks (2025-12-20)
617
+
618
+ Run: `snapshots/benchmarks/20397054933` (commit `25e4fdf`, oha, 50 concurrency, 10s, Linux x86_64).
619
+
620
+ | Metric | Value |
621
+ | --- | --- |
622
+ | Avg RPS (all workloads) | 8,271 |
623
+ | Avg latency (ms) | 6.50 |
624
+
625
+ Category breakdown:
626
+
627
+ | Category | Avg RPS | Avg latency (ms) |
628
+ | --- | --- | --- |
629
+ | path-params | 9,591 | 5.22 |
630
+ | json-bodies | 8,648 | 5.78 |
631
+ | forms | 7,989 | 6.27 |
632
+ | query-params | 7,984 | 6.33 |
633
+ | multipart | 5,604 | 10.36 |
634
+
620
635
  ## Examples
621
636
 
622
637
  The [examples directory](../../examples/) contains comprehensive demonstrations:
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-rb-ext"
3
- version = "0.3.6"
3
+ version = "0.5.0"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
@@ -12,6 +12,6 @@ crate-type = ["cdylib"]
12
12
  [workspace]
13
13
 
14
14
  [dependencies]
15
- magnus = { git = "https://github.com/matsadler/magnus", rev = "f6db11769efb517427bf7f121f9c32e18b059b38", features = ["rb-sys"] }
15
+ magnus = { version = "0.8.2", features = ["rb-sys"] }
16
16
  # Use vendored crates for packaged gem distribution
17
17
  spikard_rb_core = { package = "spikard-rb", path = "../../vendor/crates/spikard-rb" }
data/lib/spikard/app.rb CHANGED
@@ -119,7 +119,7 @@ module Spikard
119
119
 
120
120
  HTTP_METHODS = %w[GET POST PUT PATCH DELETE OPTIONS HEAD TRACE].freeze
121
121
  SUPPORTED_OPTIONS = %i[request_schema response_schema parameter_schema file_params is_async cors
122
- body_param_name].freeze
122
+ body_param_name jsonrpc_method].freeze
123
123
 
124
124
  attr_reader :routes
125
125
 
@@ -138,19 +138,38 @@ module Spikard
138
138
  handler_name = handler_name&.to_s
139
139
  validate_route_arguments!(block, options)
140
140
  metadata = if defined?(Spikard::Native) && Spikard::Native.respond_to?(:build_route_metadata)
141
- Spikard::Native.build_route_metadata(
142
- method,
143
- path,
144
- handler_name,
145
- options[:request_schema],
146
- options[:response_schema],
147
- options[:parameter_schema],
148
- options[:file_params],
149
- options.fetch(:is_async, false),
150
- options[:cors],
151
- options[:body_param_name]&.to_s,
152
- block
153
- )
141
+ begin
142
+ Spikard::Native.build_route_metadata(
143
+ method,
144
+ path,
145
+ handler_name,
146
+ options[:request_schema],
147
+ options[:response_schema],
148
+ options[:parameter_schema],
149
+ options[:file_params],
150
+ options.fetch(:is_async, false),
151
+ options[:cors],
152
+ options[:body_param_name]&.to_s,
153
+ options[:jsonrpc_method],
154
+ block
155
+ )
156
+ rescue ArgumentError => e
157
+ raise unless e.message.include?('wrong number of arguments')
158
+
159
+ Spikard::Native.build_route_metadata(
160
+ method,
161
+ path,
162
+ handler_name,
163
+ options[:request_schema],
164
+ options[:response_schema],
165
+ options[:parameter_schema],
166
+ options[:file_params],
167
+ options.fetch(:is_async, false),
168
+ options[:cors],
169
+ options[:body_param_name]&.to_s,
170
+ block
171
+ )
172
+ end
154
173
  else
155
174
  handler_name ||= default_handler_name(method, path)
156
175
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'timeout'
4
5
 
5
6
  module Spikard
6
7
  # Testing helpers that wrap the native Ruby extension.
@@ -8,25 +9,37 @@ module Spikard
8
9
  module_function
9
10
 
10
11
  def create_test_client(app, config: nil)
11
- unless defined?(Spikard::Native::TestClient)
12
- raise LoadError, 'Spikard native test client is not available. Build the native extension before running tests.'
13
- end
12
+ trace('create_test_client:start')
13
+ ensure_native_test_client!
14
+ config = resolve_test_config(app, config)
15
+ native = build_native_test_client(app, config)
16
+ trace('create_test_client:done')
17
+ TestClient.new(native)
18
+ end
14
19
 
15
- # Allow generated apps to stash a test config
16
- if config.nil? && app.instance_variable_defined?(:@__spikard_test_config)
17
- config = app.instance_variable_get(:@__spikard_test_config)
20
+ def ensure_native_test_client!
21
+ return if defined?(Spikard::Native::TestClient)
22
+
23
+ raise LoadError, 'Spikard native test client is not available. Build the native extension before running tests.'
24
+ end
25
+
26
+ def resolve_test_config(app, config)
27
+ return config if config
28
+
29
+ if app.instance_variable_defined?(:@__spikard_test_config)
30
+ return app.instance_variable_get(:@__spikard_test_config)
18
31
  end
19
32
 
20
- # Use default config if none provided
21
- config ||= Spikard::ServerConfig.new
33
+ Spikard::ServerConfig.new
34
+ end
22
35
 
36
+ def build_native_test_client(app, config)
23
37
  routes_json = app.normalized_routes_json
24
38
  handlers = app.handler_map.transform_keys(&:to_sym)
25
39
  ws_handlers = app.websocket_handlers || {}
26
40
  sse_producers = app.sse_producers || {}
27
41
  dependencies = app.dependencies || {}
28
- native = Spikard::Native::TestClient.new(routes_json, handlers, config, ws_handlers, sse_producers, dependencies)
29
- TestClient.new(native)
42
+ Spikard::Native::TestClient.new(routes_json, handlers, config, ws_handlers, sse_producers, dependencies)
30
43
  end
31
44
 
32
45
  # High level wrapper around the native test client.
@@ -50,7 +63,9 @@ module Spikard
50
63
  end
51
64
 
52
65
  def websocket(path)
66
+ Testing.trace("websocket:start #{path}")
53
67
  native_ws = @native.websocket(path)
68
+ Testing.trace("websocket:connected #{path}")
54
69
  WebSocketTestConnection.new(native_ws)
55
70
  end
56
71
 
@@ -77,22 +92,26 @@ module Spikard
77
92
  end
78
93
 
79
94
  def send_text(text)
95
+ Testing.trace('websocket:send_text')
80
96
  @native_ws.send_text(JSON.generate(text))
81
97
  end
82
98
 
83
99
  def send_json(obj)
100
+ Testing.trace('websocket:send_json')
84
101
  @native_ws.send_json(obj)
85
102
  end
86
103
 
87
104
  def receive_text
88
- raw = @native_ws.receive_text
105
+ Testing.trace('websocket:receive_text')
106
+ raw = with_timeout { @native_ws.receive_text }
89
107
  JSON.parse(raw)
90
108
  rescue JSON::ParserError
91
109
  raw
92
110
  end
93
111
 
94
112
  def receive_json
95
- @native_ws.receive_json
113
+ Testing.trace('websocket:receive_json')
114
+ with_timeout { @native_ws.receive_json }
96
115
  end
97
116
 
98
117
  def receive_bytes
@@ -105,8 +124,18 @@ module Spikard
105
124
  end
106
125
 
107
126
  def close
127
+ Testing.trace('websocket:close')
108
128
  @native_ws.close
109
129
  end
130
+
131
+ private
132
+
133
+ def with_timeout(&)
134
+ timeout_ms = ENV.fetch('SPIKARD_RB_TEST_TIMEOUT_MS', nil)
135
+ return yield unless timeout_ms
136
+
137
+ Timeout.timeout(timeout_ms.to_f / 1000.0, &)
138
+ end
110
139
  end
111
140
 
112
141
  # WebSocket message wrapper
@@ -217,5 +246,11 @@ module Spikard
217
246
  JSON.parse(@data)
218
247
  end
219
248
  end
249
+
250
+ def trace(message)
251
+ return unless ENV['SPIKARD_RB_TEST_TRACE'] == '1'
252
+
253
+ warn("[spikard-rb-test] #{message}")
254
+ end
220
255
  end
221
256
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spikard
4
- VERSION = '0.3.6'
4
+ VERSION = '0.5.0'
5
5
  end
@@ -0,0 +1,63 @@
1
+ [package]
2
+ name = "spikard-bindings-shared"
3
+ version = "0.5.0"
4
+ edition = "2024"
5
+ authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
6
+ license = "MIT"
7
+ repository = "https://github.com/Goldziher/spikard"
8
+ homepage = "https://github.com/Goldziher/spikard"
9
+
10
+ [dependencies]
11
+ serde = { version = "1.0", features = ["derive"] }
12
+ serde_json = "1.0"
13
+ axum = { version = "0.8", features = ["multipart", "ws"] }
14
+ tokio = { version = "1", features = ["full"] }
15
+ thiserror = "2.0"
16
+ spikard-core = { path = "../spikard-core", features = ["di"] }
17
+ spikard-http = { path = "../spikard-http", features = ["di"] }
18
+ tracing = "0.1"
19
+ http = "1.4"
20
+ http-body-util = "0.1"
21
+
22
+ [features]
23
+ default = []
24
+ python-support = ["pyo3", "pyo3-async-runtimes"]
25
+ node-support = ["napi", "napi-derive"]
26
+ ruby-support = ["magnus"]
27
+ php-support = ["ext-php-rs"]
28
+ wasm-support = ["wasm-bindgen"]
29
+
30
+ [dependencies.pyo3]
31
+ version = "0.27"
32
+ optional = true
33
+ features = ["abi3-py310"]
34
+
35
+ [dependencies.pyo3-async-runtimes]
36
+ version = "0.27"
37
+ optional = true
38
+ features = ["tokio-runtime"]
39
+
40
+ [dependencies.napi]
41
+ version = "3"
42
+ optional = true
43
+ default-features = false
44
+ features = ["napi9", "async"]
45
+
46
+ [dependencies.napi-derive]
47
+ version = "3"
48
+ optional = true
49
+
50
+ [dependencies.magnus]
51
+ version = "0.8.2"
52
+ optional = true
53
+
54
+ [dependencies.ext-php-rs]
55
+ version = "0.15"
56
+ optional = true
57
+
58
+ [dependencies.wasm-bindgen]
59
+ version = "0.2"
60
+ optional = true
61
+
62
+ [dev-dependencies]
63
+ pretty_assertions = "1.4"
@@ -0,0 +1,132 @@
1
+ //! Example of implementing ConfigSource for a language binding
2
+ //!
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.
5
+
6
+ use spikard_bindings_shared::{ConfigExtractor, ConfigSource};
7
+ use std::collections::HashMap;
8
+
9
+ /// Example: PyO3 Python dict wrapper
10
+ struct PyDictWrapper {
11
+ data: HashMap<String, String>,
12
+ }
13
+
14
+ impl PyDictWrapper {
15
+ fn new() -> Self {
16
+ Self { data: HashMap::new() }
17
+ }
18
+
19
+ fn insert(&mut self, key: &str, value: &str) {
20
+ self.data.insert(key.to_string(), value.to_string());
21
+ }
22
+ }
23
+
24
+ impl ConfigSource for PyDictWrapper {
25
+ fn get_bool(&self, key: &str) -> Option<bool> {
26
+ self.data.get(key).and_then(|v| match v.as_str() {
27
+ "true" | "True" => Some(true),
28
+ "false" | "False" => Some(false),
29
+ _ => v.parse().ok(),
30
+ })
31
+ }
32
+
33
+ fn get_u64(&self, key: &str) -> Option<u64> {
34
+ self.data.get(key).and_then(|v| v.parse().ok())
35
+ }
36
+
37
+ fn get_u16(&self, key: &str) -> Option<u16> {
38
+ self.data.get(key).and_then(|v| v.parse().ok())
39
+ }
40
+
41
+ fn get_string(&self, key: &str) -> Option<String> {
42
+ self.data.get(key).cloned()
43
+ }
44
+
45
+ fn get_vec_string(&self, key: &str) -> Option<Vec<String>> {
46
+ self.data
47
+ .get(key)
48
+ .map(|s| s.split(',').map(|item| item.trim().to_string()).collect())
49
+ }
50
+
51
+ fn get_nested(&self, _key: &str) -> Option<Box<dyn ConfigSource + '_>> {
52
+ None
53
+ }
54
+
55
+ fn has_key(&self, key: &str) -> bool {
56
+ self.data.contains_key(key)
57
+ }
58
+ }
59
+
60
+ fn main() {
61
+ println!("=== ConfigExtractor Example ===\n");
62
+
63
+ println!("1. Extracting compression configuration:");
64
+ let mut compression_config = PyDictWrapper::new();
65
+ compression_config.insert("gzip", "true");
66
+ compression_config.insert("brotli", "true");
67
+ compression_config.insert("min_size", "2048");
68
+ compression_config.insert("quality", "9");
69
+
70
+ match ConfigExtractor::extract_compression_config(&compression_config) {
71
+ Ok(config) => {
72
+ println!(" gzip: {}", config.gzip);
73
+ println!(" brotli: {}", config.brotli);
74
+ println!(" min_size: {}", config.min_size);
75
+ println!(" quality: {}\n", config.quality);
76
+ }
77
+ Err(e) => println!(" Error: {}\n", e),
78
+ }
79
+
80
+ println!("2. Extracting JWT authentication configuration:");
81
+ let mut jwt_config = PyDictWrapper::new();
82
+ jwt_config.insert("secret", "my-secret-key");
83
+ jwt_config.insert("algorithm", "HS256");
84
+ jwt_config.insert("leeway", "60");
85
+
86
+ match ConfigExtractor::extract_jwt_config(&jwt_config) {
87
+ Ok(config) => {
88
+ println!(" secret: [REDACTED]");
89
+ println!(" algorithm: {}", config.algorithm);
90
+ println!(" leeway: {}\n", config.leeway);
91
+ }
92
+ Err(e) => println!(" Error: {}\n", e),
93
+ }
94
+
95
+ println!("3. Extracting API Key authentication configuration:");
96
+ let mut api_key_config = PyDictWrapper::new();
97
+ api_key_config.insert("keys", "key1,key2,key3");
98
+ api_key_config.insert("header_name", "X-API-Key");
99
+
100
+ match ConfigExtractor::extract_api_key_config(&api_key_config) {
101
+ Ok(config) => {
102
+ println!(" keys: {:?}", config.keys);
103
+ println!(" header_name: {}\n", config.header_name);
104
+ }
105
+ Err(e) => println!(" Error: {}\n", e),
106
+ }
107
+
108
+ println!("4. Extracting rate limit configuration:");
109
+ let mut rate_limit_config = PyDictWrapper::new();
110
+ rate_limit_config.insert("per_second", "100");
111
+ rate_limit_config.insert("burst", "20");
112
+ rate_limit_config.insert("ip_based", "true");
113
+
114
+ match ConfigExtractor::extract_rate_limit_config(&rate_limit_config) {
115
+ Ok(config) => {
116
+ println!(" per_second: {}", config.per_second);
117
+ println!(" burst: {}", config.burst);
118
+ println!(" ip_based: {}\n", config.ip_based);
119
+ }
120
+ Err(e) => println!(" Error: {}\n", e),
121
+ }
122
+
123
+ println!("5. Testing error handling (missing 'burst' field):");
124
+ let rate_limit_config = PyDictWrapper::new();
125
+
126
+ match ConfigExtractor::extract_rate_limit_config(&rate_limit_config) {
127
+ Ok(_config) => println!(" Success (unexpected!)"),
128
+ Err(e) => println!(" Expected error: {}\n", e),
129
+ }
130
+
131
+ println!("=== Example Complete ===");
132
+ }