spikard 0.5.0 → 0.6.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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +674 -674
  4. data/ext/spikard_rb/Cargo.toml +17 -17
  5. data/ext/spikard_rb/extconf.rb +13 -10
  6. data/ext/spikard_rb/src/lib.rs +6 -6
  7. data/lib/spikard/app.rb +405 -405
  8. data/lib/spikard/background.rb +27 -27
  9. data/lib/spikard/config.rb +396 -396
  10. data/lib/spikard/converters.rb +13 -13
  11. data/lib/spikard/handler_wrapper.rb +113 -113
  12. data/lib/spikard/provide.rb +214 -214
  13. data/lib/spikard/response.rb +173 -173
  14. data/lib/spikard/schema.rb +243 -243
  15. data/lib/spikard/sse.rb +111 -111
  16. data/lib/spikard/streaming_response.rb +44 -44
  17. data/lib/spikard/testing.rb +256 -256
  18. data/lib/spikard/upload_file.rb +131 -131
  19. data/lib/spikard/version.rb +5 -5
  20. data/lib/spikard/websocket.rb +59 -59
  21. data/lib/spikard.rb +43 -43
  22. data/sig/spikard.rbs +366 -366
  23. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -63
  24. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -132
  25. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -752
  26. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -194
  27. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -246
  28. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -401
  29. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -238
  30. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -24
  31. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -292
  32. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -616
  33. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -305
  34. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -248
  35. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -351
  36. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -454
  37. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -383
  38. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -280
  39. data/vendor/crates/spikard-core/Cargo.toml +40 -40
  40. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
  41. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
  42. data/vendor/crates/spikard-core/src/debug.rs +127 -127
  43. data/vendor/crates/spikard-core/src/di/container.rs +702 -702
  44. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
  45. data/vendor/crates/spikard-core/src/di/error.rs +118 -118
  46. data/vendor/crates/spikard-core/src/di/factory.rs +534 -534
  47. data/vendor/crates/spikard-core/src/di/graph.rs +506 -506
  48. data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
  49. data/vendor/crates/spikard-core/src/di/resolved.rs +405 -405
  50. data/vendor/crates/spikard-core/src/di/value.rs +281 -281
  51. data/vendor/crates/spikard-core/src/errors.rs +69 -69
  52. data/vendor/crates/spikard-core/src/http.rs +415 -415
  53. data/vendor/crates/spikard-core/src/lib.rs +29 -29
  54. data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -1186
  55. data/vendor/crates/spikard-core/src/metadata.rs +389 -389
  56. data/vendor/crates/spikard-core/src/parameters.rs +2525 -2525
  57. data/vendor/crates/spikard-core/src/problem.rs +344 -344
  58. data/vendor/crates/spikard-core/src/request_data.rs +1154 -1154
  59. data/vendor/crates/spikard-core/src/router.rs +510 -510
  60. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
  61. data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
  62. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +696 -688
  63. data/vendor/crates/spikard-core/src/validation/mod.rs +457 -457
  64. data/vendor/crates/spikard-http/Cargo.toml +62 -64
  65. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -148
  66. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -92
  67. data/vendor/crates/spikard-http/src/auth.rs +296 -296
  68. data/vendor/crates/spikard-http/src/background.rs +1860 -1860
  69. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
  70. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
  71. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
  72. data/vendor/crates/spikard-http/src/cors.rs +1005 -1005
  73. data/vendor/crates/spikard-http/src/debug.rs +128 -128
  74. data/vendor/crates/spikard-http/src/di_handler.rs +1668 -1668
  75. data/vendor/crates/spikard-http/src/handler_response.rs +901 -901
  76. data/vendor/crates/spikard-http/src/handler_trait.rs +838 -830
  77. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -290
  78. data/vendor/crates/spikard-http/src/lib.rs +534 -534
  79. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -230
  80. data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -1193
  81. data/vendor/crates/spikard-http/src/middleware/mod.rs +560 -540
  82. data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -912
  83. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -513
  84. data/vendor/crates/spikard-http/src/middleware/validation.rs +768 -735
  85. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
  86. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -535
  87. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -1363
  88. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -665
  89. data/vendor/crates/spikard-http/src/query_parser.rs +793 -793
  90. data/vendor/crates/spikard-http/src/response.rs +720 -720
  91. data/vendor/crates/spikard-http/src/server/handler.rs +1650 -1650
  92. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -234
  93. data/vendor/crates/spikard-http/src/server/mod.rs +1593 -1502
  94. data/vendor/crates/spikard-http/src/server/request_extraction.rs +789 -770
  95. data/vendor/crates/spikard-http/src/server/routing_factory.rs +629 -599
  96. data/vendor/crates/spikard-http/src/sse.rs +1409 -1409
  97. data/vendor/crates/spikard-http/src/testing/form.rs +52 -52
  98. data/vendor/crates/spikard-http/src/testing/multipart.rs +64 -60
  99. data/vendor/crates/spikard-http/src/testing/test_client.rs +311 -283
  100. data/vendor/crates/spikard-http/src/testing.rs +406 -377
  101. data/vendor/crates/spikard-http/src/websocket.rs +1404 -1375
  102. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -832
  103. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -309
  104. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -26
  105. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -192
  106. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -5
  107. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -1093
  108. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -656
  109. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -314
  110. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -620
  111. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -663
  112. data/vendor/crates/spikard-rb/Cargo.toml +48 -48
  113. data/vendor/crates/spikard-rb/build.rs +199 -199
  114. data/vendor/crates/spikard-rb/src/background.rs +63 -63
  115. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -5
  116. data/vendor/crates/spikard-rb/src/config/server_config.rs +285 -285
  117. data/vendor/crates/spikard-rb/src/conversion.rs +554 -554
  118. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -100
  119. data/vendor/crates/spikard-rb/src/di/mod.rs +375 -375
  120. data/vendor/crates/spikard-rb/src/handler.rs +618 -618
  121. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -3
  122. data/vendor/crates/spikard-rb/src/lib.rs +1806 -1810
  123. data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -275
  124. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -5
  125. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +442 -447
  126. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -5
  127. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -324
  128. data/vendor/crates/spikard-rb/src/server.rs +305 -308
  129. data/vendor/crates/spikard-rb/src/sse.rs +231 -231
  130. data/vendor/crates/spikard-rb/src/testing/client.rs +538 -551
  131. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -7
  132. data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -143
  133. data/vendor/crates/spikard-rb/src/testing/websocket.rs +608 -635
  134. data/vendor/crates/spikard-rb/src/websocket.rs +377 -374
  135. metadata +15 -1
@@ -1,214 +1,214 @@
1
- # frozen_string_literal: true
2
-
3
- module Spikard
4
- # Wrapper class for dependency providers
5
- #
6
- # This class wraps factory functions and configuration for dependency injection.
7
- # It provides a consistent API across Python, Node.js, and Ruby bindings.
8
- #
9
- # @example Factory with caching
10
- # app.provide("db", Spikard::Provide.new(method("create_db"), cacheable: true))
11
- #
12
- # @example Factory with dependencies
13
- # app.provide("auth", Spikard::Provide.new(
14
- # method("create_auth_service"),
15
- # depends_on: ["db", "cache"],
16
- # singleton: true
17
- # ))
18
- class Provide
19
- attr_reader :factory, :depends_on, :singleton, :cacheable
20
-
21
- # Create a new dependency provider
22
- #
23
- # @param factory [Proc, Method] The factory function that creates the dependency value
24
- # @param depends_on [Array<String, Symbol>] List of dependency keys this factory depends on
25
- # @param singleton [Boolean] Whether to cache the value globally (default: false)
26
- # @param cacheable [Boolean] Whether to cache the value per-request (default: true)
27
- def initialize(factory, depends_on: [], singleton: false, cacheable: true)
28
- @factory = factory
29
- @depends_on = Array(depends_on).map(&:to_s)
30
- @singleton = singleton
31
- @cacheable = cacheable
32
- end
33
-
34
- # Check if the factory is async (based on method arity or other heuristics)
35
- #
36
- # @return [Boolean] True if the factory appears to be async
37
- def async?
38
- # Ruby doesn't have explicit async/await like Python/JS
39
- # We could check if it returns a Thread or uses Fiber
40
- false
41
- end
42
-
43
- # Check if the factory is an async generator
44
- #
45
- # @return [Boolean] True if the factory is an async generator
46
- def async_generator?
47
- false
48
- end
49
- end
50
-
51
- # Dependency Injection support for Spikard applications
52
- #
53
- # Provides methods for registering and managing dependencies that can be
54
- # automatically injected into route handlers.
55
- #
56
- # @example Registering a value dependency
57
- # app.provide("database_url", "postgresql://localhost/mydb")
58
- #
59
- # @example Registering a factory dependency
60
- # app.provide("db_pool", depends_on: ["database_url"]) do |database_url:|
61
- # ConnectionPool.new(database_url)
62
- # end
63
- #
64
- # @example Singleton dependency (shared across all requests)
65
- # app.provide("config", singleton: true) do
66
- # Config.load_from_file("config.yml")
67
- # end
68
- #
69
- # @example Using Provide wrapper
70
- # app.provide("db", Spikard::Provide.new(method("create_db"), cacheable: true))
71
- module ProvideSupport
72
- # Register a dependency in the DI container
73
- #
74
- # This method supports three patterns:
75
- # 1. **Value dependency**: Pass a value directly (e.g., string, number, object)
76
- # 2. **Factory dependency**: Pass a block that computes the value
77
- # 3. **Provide wrapper**: Pass a Spikard::Provide instance
78
- #
79
- # @param key [String, Symbol] Unique identifier for the dependency
80
- # @param value [Object, Provide, nil] Static value, Provide instance, or nil
81
- # @param depends_on [Array<String, Symbol>] List of dependency keys this factory depends on
82
- # @param singleton [Boolean] Whether to cache the value globally (default: false)
83
- # @param cacheable [Boolean] Whether to cache the value per-request (default: true)
84
- # @yield Optional factory block that receives dependencies as keyword arguments
85
- # @yieldparam **deps [Hash] Resolved dependencies as keyword arguments
86
- # @yieldreturn [Object] The computed dependency value
87
- # @return [self] Returns self for method chaining
88
- #
89
- # @example Value dependency
90
- # app.provide("app_name", "MyApp")
91
- # app.provide("port", 8080)
92
- #
93
- # @example Factory with dependencies
94
- # app.provide("database", depends_on: ["config"]) do |config:|
95
- # Database.connect(config["db_url"])
96
- # end
97
- #
98
- # @example Singleton factory
99
- # app.provide("thread_pool", singleton: true) do
100
- # ThreadPool.new(size: 10)
101
- # end
102
- #
103
- # @example Non-cacheable factory (resolves every time)
104
- # app.provide("request_id", cacheable: false) do
105
- # SecureRandom.uuid
106
- # end
107
- #
108
- # @example Using Provide wrapper
109
- # app.provide("db", Spikard::Provide.new(method("create_db"), cacheable: true))
110
- def provide(key, value = nil, depends_on: [], singleton: false, cacheable: true, &block)
111
- key_str = key.to_s
112
- registry = ensure_native_dependencies!
113
-
114
- # Handle Provide wrapper instances
115
- if value.is_a?(Provide)
116
- registry.register_factory(key_str, value.factory, value.depends_on, value.singleton, value.cacheable)
117
- elsif block
118
- registry.register_factory(key_str, block, Array(depends_on).map(&:to_s), singleton, cacheable)
119
- else
120
- raise ArgumentError, 'Either provide a value or a block, not both' if value.nil?
121
-
122
- registry.register_value(key_str, value)
123
- end
124
-
125
- self
126
- end
127
-
128
- # Get all registered dependencies
129
- #
130
- # @return [Hash] Dictionary mapping dependency keys to their definitions
131
- # @api private
132
- def dependencies
133
- ensure_native_dependencies!
134
- end
135
-
136
- private
137
-
138
- def ensure_native_dependencies!
139
- registry = (@native_dependencies if instance_variable_defined?(:@native_dependencies) && @native_dependencies)
140
- raise 'Spikard native dependency registry unavailable' unless registry
141
-
142
- registry
143
- end
144
- end
145
-
146
- # Dependency injection handler wrapper
147
- #
148
- # Wraps a route handler to inject dependencies based on parameter names.
149
- # Dependencies are resolved from the DI container and passed as keyword arguments.
150
- #
151
- # @api private
152
- module DIHandlerWrapper
153
- # Wrap a handler to inject dependencies
154
- #
155
- # @param handler [Proc] The original route handler
156
- # @param dependencies [Hash] Available dependencies from the app
157
- # @return [Proc] Wrapped handler with DI support
158
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
159
- def self.wrap_handler(handler, dependencies)
160
- # Extract parameter names from the handler
161
- params = handler.parameters.map { |_type, name| name.to_s }
162
-
163
- # Find which parameters match registered dependencies
164
- injectable_params = params & dependencies.keys
165
-
166
- if injectable_params.empty?
167
- # No DI needed, return original handler
168
- return handler
169
- end
170
-
171
- # Create wrapped handler that injects dependencies
172
- lambda do |request|
173
- # Build kwargs with injected dependencies
174
- kwargs = {}
175
-
176
- injectable_params.each do |param_name|
177
- dep_def = dependencies[param_name]
178
- kwargs[param_name.to_sym] = resolve_dependency(dep_def, request)
179
- end
180
-
181
- # Call original handler with injected dependencies
182
- if handler.arity.zero?
183
- # Handler takes no arguments (dependencies injected via closure or instance vars)
184
- handler.call
185
- elsif injectable_params.length == params.length
186
- # All parameters are dependencies
187
- handler.call(**kwargs)
188
- else
189
- # Mix of request data and dependencies
190
- handler.call(request, **kwargs)
191
- end
192
- end
193
- end
194
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
195
-
196
- # Resolve a dependency definition
197
- #
198
- # @param dep_def [Hash] Dependency definition
199
- # @param request [Hash] Request context (unused for now, future: per-request deps)
200
- # @return [Object] Resolved dependency value
201
- # @api private
202
- def self.resolve_dependency(dep_def, _request)
203
- case dep_def[:type]
204
- when :value
205
- dep_def[:value]
206
- when :factory
207
- factory = dep_def[:factory]
208
- dep_def[:depends_on]
209
- # TODO: Implement nested dependency resolution when dependencies are provided
210
- factory.call
211
- end
212
- end
213
- end
214
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Spikard
4
+ # Wrapper class for dependency providers
5
+ #
6
+ # This class wraps factory functions and configuration for dependency injection.
7
+ # It provides a consistent API across Python, Node.js, and Ruby bindings.
8
+ #
9
+ # @example Factory with caching
10
+ # app.provide("db", Spikard::Provide.new(method("create_db"), cacheable: true))
11
+ #
12
+ # @example Factory with dependencies
13
+ # app.provide("auth", Spikard::Provide.new(
14
+ # method("create_auth_service"),
15
+ # depends_on: ["db", "cache"],
16
+ # singleton: true
17
+ # ))
18
+ class Provide
19
+ attr_reader :factory, :depends_on, :singleton, :cacheable
20
+
21
+ # Create a new dependency provider
22
+ #
23
+ # @param factory [Proc, Method] The factory function that creates the dependency value
24
+ # @param depends_on [Array<String, Symbol>] List of dependency keys this factory depends on
25
+ # @param singleton [Boolean] Whether to cache the value globally (default: false)
26
+ # @param cacheable [Boolean] Whether to cache the value per-request (default: true)
27
+ def initialize(factory, depends_on: [], singleton: false, cacheable: true)
28
+ @factory = factory
29
+ @depends_on = Array(depends_on).map(&:to_s)
30
+ @singleton = singleton
31
+ @cacheable = cacheable
32
+ end
33
+
34
+ # Check if the factory is async (based on method arity or other heuristics)
35
+ #
36
+ # @return [Boolean] True if the factory appears to be async
37
+ def async?
38
+ # Ruby doesn't have explicit async/await like Python/JS
39
+ # We could check if it returns a Thread or uses Fiber
40
+ false
41
+ end
42
+
43
+ # Check if the factory is an async generator
44
+ #
45
+ # @return [Boolean] True if the factory is an async generator
46
+ def async_generator?
47
+ false
48
+ end
49
+ end
50
+
51
+ # Dependency Injection support for Spikard applications
52
+ #
53
+ # Provides methods for registering and managing dependencies that can be
54
+ # automatically injected into route handlers.
55
+ #
56
+ # @example Registering a value dependency
57
+ # app.provide("database_url", "postgresql://localhost/mydb")
58
+ #
59
+ # @example Registering a factory dependency
60
+ # app.provide("db_pool", depends_on: ["database_url"]) do |database_url:|
61
+ # ConnectionPool.new(database_url)
62
+ # end
63
+ #
64
+ # @example Singleton dependency (shared across all requests)
65
+ # app.provide("config", singleton: true) do
66
+ # Config.load_from_file("config.yml")
67
+ # end
68
+ #
69
+ # @example Using Provide wrapper
70
+ # app.provide("db", Spikard::Provide.new(method("create_db"), cacheable: true))
71
+ module ProvideSupport
72
+ # Register a dependency in the DI container
73
+ #
74
+ # This method supports three patterns:
75
+ # 1. **Value dependency**: Pass a value directly (e.g., string, number, object)
76
+ # 2. **Factory dependency**: Pass a block that computes the value
77
+ # 3. **Provide wrapper**: Pass a Spikard::Provide instance
78
+ #
79
+ # @param key [String, Symbol] Unique identifier for the dependency
80
+ # @param value [Object, Provide, nil] Static value, Provide instance, or nil
81
+ # @param depends_on [Array<String, Symbol>] List of dependency keys this factory depends on
82
+ # @param singleton [Boolean] Whether to cache the value globally (default: false)
83
+ # @param cacheable [Boolean] Whether to cache the value per-request (default: true)
84
+ # @yield Optional factory block that receives dependencies as keyword arguments
85
+ # @yieldparam **deps [Hash] Resolved dependencies as keyword arguments
86
+ # @yieldreturn [Object] The computed dependency value
87
+ # @return [self] Returns self for method chaining
88
+ #
89
+ # @example Value dependency
90
+ # app.provide("app_name", "MyApp")
91
+ # app.provide("port", 8080)
92
+ #
93
+ # @example Factory with dependencies
94
+ # app.provide("database", depends_on: ["config"]) do |config:|
95
+ # Database.connect(config["db_url"])
96
+ # end
97
+ #
98
+ # @example Singleton factory
99
+ # app.provide("thread_pool", singleton: true) do
100
+ # ThreadPool.new(size: 10)
101
+ # end
102
+ #
103
+ # @example Non-cacheable factory (resolves every time)
104
+ # app.provide("request_id", cacheable: false) do
105
+ # SecureRandom.uuid
106
+ # end
107
+ #
108
+ # @example Using Provide wrapper
109
+ # app.provide("db", Spikard::Provide.new(method("create_db"), cacheable: true))
110
+ def provide(key, value = nil, depends_on: [], singleton: false, cacheable: true, &block)
111
+ key_str = key.to_s
112
+ registry = ensure_native_dependencies!
113
+
114
+ # Handle Provide wrapper instances
115
+ if value.is_a?(Provide)
116
+ registry.register_factory(key_str, value.factory, value.depends_on, value.singleton, value.cacheable)
117
+ elsif block
118
+ registry.register_factory(key_str, block, Array(depends_on).map(&:to_s), singleton, cacheable)
119
+ else
120
+ raise ArgumentError, 'Either provide a value or a block, not both' if value.nil?
121
+
122
+ registry.register_value(key_str, value)
123
+ end
124
+
125
+ self
126
+ end
127
+
128
+ # Get all registered dependencies
129
+ #
130
+ # @return [Hash] Dictionary mapping dependency keys to their definitions
131
+ # @api private
132
+ def dependencies
133
+ ensure_native_dependencies!
134
+ end
135
+
136
+ private
137
+
138
+ def ensure_native_dependencies!
139
+ registry = (@native_dependencies if instance_variable_defined?(:@native_dependencies) && @native_dependencies)
140
+ raise 'Spikard native dependency registry unavailable' unless registry
141
+
142
+ registry
143
+ end
144
+ end
145
+
146
+ # Dependency injection handler wrapper
147
+ #
148
+ # Wraps a route handler to inject dependencies based on parameter names.
149
+ # Dependencies are resolved from the DI container and passed as keyword arguments.
150
+ #
151
+ # @api private
152
+ module DIHandlerWrapper
153
+ # Wrap a handler to inject dependencies
154
+ #
155
+ # @param handler [Proc] The original route handler
156
+ # @param dependencies [Hash] Available dependencies from the app
157
+ # @return [Proc] Wrapped handler with DI support
158
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
159
+ def self.wrap_handler(handler, dependencies)
160
+ # Extract parameter names from the handler
161
+ params = handler.parameters.map { |_type, name| name.to_s }
162
+
163
+ # Find which parameters match registered dependencies
164
+ injectable_params = params & dependencies.keys
165
+
166
+ if injectable_params.empty?
167
+ # No DI needed, return original handler
168
+ return handler
169
+ end
170
+
171
+ # Create wrapped handler that injects dependencies
172
+ lambda do |request|
173
+ # Build kwargs with injected dependencies
174
+ kwargs = {}
175
+
176
+ injectable_params.each do |param_name|
177
+ dep_def = dependencies[param_name]
178
+ kwargs[param_name.to_sym] = resolve_dependency(dep_def, request)
179
+ end
180
+
181
+ # Call original handler with injected dependencies
182
+ if handler.arity.zero?
183
+ # Handler takes no arguments (dependencies injected via closure or instance vars)
184
+ handler.call
185
+ elsif injectable_params.length == params.length
186
+ # All parameters are dependencies
187
+ handler.call(**kwargs)
188
+ else
189
+ # Mix of request data and dependencies
190
+ handler.call(request, **kwargs)
191
+ end
192
+ end
193
+ end
194
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
195
+
196
+ # Resolve a dependency definition
197
+ #
198
+ # @param dep_def [Hash] Dependency definition
199
+ # @param request [Hash] Request context (unused for now, future: per-request deps)
200
+ # @return [Object] Resolved dependency value
201
+ # @api private
202
+ def self.resolve_dependency(dep_def, _request)
203
+ case dep_def[:type]
204
+ when :value
205
+ dep_def[:value]
206
+ when :factory
207
+ factory = dep_def[:factory]
208
+ dep_def[:depends_on]
209
+ # TODO: Implement nested dependency resolution when dependencies are provided
210
+ factory.call
211
+ end
212
+ end
213
+ end
214
+ end