spikard 0.12.0 → 0.15.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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/Steepfile +6 -0
  3. data/ext/spikard_rb/extconf.rb +1 -2
  4. data/ext/spikard_rb/{Cargo.lock → native/Cargo.lock} +897 -451
  5. data/ext/spikard_rb/native/Cargo.toml +24 -0
  6. data/ext/spikard_rb/src/lib.rs +5366 -3
  7. data/lib/spikard/native.rb +86 -0
  8. data/lib/spikard/version.rb +6 -1
  9. data/lib/spikard.rb +8 -45
  10. data/lib/spikard_rb.so +0 -0
  11. data/sig/types.rbs +427 -0
  12. metadata +14 -242
  13. data/LICENSE +0 -1
  14. data/README.md +0 -267
  15. data/ext/spikard_rb/Cargo.toml +0 -17
  16. data/lib/spikard/app.rb +0 -428
  17. data/lib/spikard/background.rb +0 -58
  18. data/lib/spikard/config.rb +0 -506
  19. data/lib/spikard/converters.rb +0 -13
  20. data/lib/spikard/grpc.rb +0 -182
  21. data/lib/spikard/handler_wrapper.rb +0 -113
  22. data/lib/spikard/provide.rb +0 -214
  23. data/lib/spikard/response.rb +0 -173
  24. data/lib/spikard/schema.rb +0 -243
  25. data/lib/spikard/sse.rb +0 -111
  26. data/lib/spikard/streaming_response.rb +0 -44
  27. data/lib/spikard/testing.rb +0 -432
  28. data/lib/spikard/upload_file.rb +0 -131
  29. data/lib/spikard/websocket.rb +0 -59
  30. data/sig/spikard.rbs +0 -719
  31. data/vendor/crates/spikard-bindings-shared/Cargo.toml +0 -80
  32. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +0 -132
  33. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +0 -905
  34. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +0 -210
  35. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +0 -252
  36. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +0 -404
  37. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +0 -199
  38. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +0 -252
  39. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +0 -829
  40. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +0 -587
  41. data/vendor/crates/spikard-bindings-shared/src/lib.rs +0 -33
  42. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +0 -298
  43. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +0 -594
  44. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +0 -743
  45. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +0 -944
  46. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +0 -260
  47. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +0 -369
  48. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +0 -192
  49. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +0 -383
  50. data/vendor/crates/spikard-bindings-shared/tests/full_coverage.rs +0 -459
  51. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +0 -280
  52. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +0 -669
  53. data/vendor/crates/spikard-core/Cargo.toml +0 -60
  54. data/vendor/crates/spikard-core/src/bindings/mod.rs +0 -3
  55. data/vendor/crates/spikard-core/src/bindings/response.rs +0 -130
  56. data/vendor/crates/spikard-core/src/debug.rs +0 -127
  57. data/vendor/crates/spikard-core/src/di/container.rs +0 -702
  58. data/vendor/crates/spikard-core/src/di/dependency.rs +0 -273
  59. data/vendor/crates/spikard-core/src/di/error.rs +0 -118
  60. data/vendor/crates/spikard-core/src/di/factory.rs +0 -538
  61. data/vendor/crates/spikard-core/src/di/graph.rs +0 -507
  62. data/vendor/crates/spikard-core/src/di/mod.rs +0 -192
  63. data/vendor/crates/spikard-core/src/di/resolved.rs +0 -428
  64. data/vendor/crates/spikard-core/src/di/value.rs +0 -282
  65. data/vendor/crates/spikard-core/src/errors.rs +0 -72
  66. data/vendor/crates/spikard-core/src/http.rs +0 -492
  67. data/vendor/crates/spikard-core/src/lib.rs +0 -29
  68. data/vendor/crates/spikard-core/src/lifecycle.rs +0 -1273
  69. data/vendor/crates/spikard-core/src/metadata.rs +0 -378
  70. data/vendor/crates/spikard-core/src/parameters.rs +0 -2546
  71. data/vendor/crates/spikard-core/src/problem.rs +0 -358
  72. data/vendor/crates/spikard-core/src/request_data.rs +0 -1146
  73. data/vendor/crates/spikard-core/src/router.rs +0 -530
  74. data/vendor/crates/spikard-core/src/schema_registry.rs +0 -197
  75. data/vendor/crates/spikard-core/src/type_hints.rs +0 -311
  76. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +0 -710
  77. data/vendor/crates/spikard-core/src/validation/mod.rs +0 -470
  78. data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +0 -136
  79. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +0 -37
  80. data/vendor/crates/spikard-core/tests/error_mapper.rs +0 -761
  81. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +0 -106
  82. data/vendor/crates/spikard-core/tests/parameters_full.rs +0 -701
  83. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +0 -301
  84. data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +0 -67
  85. data/vendor/crates/spikard-core/tests/validation_coverage.rs +0 -250
  86. data/vendor/crates/spikard-core/tests/validation_error_paths.rs +0 -45
  87. data/vendor/crates/spikard-http/Cargo.toml +0 -87
  88. data/vendor/crates/spikard-http/examples/sse-notifications.rs +0 -148
  89. data/vendor/crates/spikard-http/examples/websocket-chat.rs +0 -92
  90. data/vendor/crates/spikard-http/src/auth.rs +0 -301
  91. data/vendor/crates/spikard-http/src/background.rs +0 -1860
  92. data/vendor/crates/spikard-http/src/bindings/mod.rs +0 -3
  93. data/vendor/crates/spikard-http/src/bindings/response.rs +0 -1
  94. data/vendor/crates/spikard-http/src/body_metadata.rs +0 -8
  95. data/vendor/crates/spikard-http/src/cors.rs +0 -1026
  96. data/vendor/crates/spikard-http/src/debug.rs +0 -128
  97. data/vendor/crates/spikard-http/src/di_handler.rs +0 -1672
  98. data/vendor/crates/spikard-http/src/grpc/framing.rs +0 -469
  99. data/vendor/crates/spikard-http/src/grpc/handler.rs +0 -1122
  100. data/vendor/crates/spikard-http/src/grpc/mod.rs +0 -434
  101. data/vendor/crates/spikard-http/src/grpc/service.rs +0 -622
  102. data/vendor/crates/spikard-http/src/grpc/streaming.rs +0 -319
  103. data/vendor/crates/spikard-http/src/handler_response.rs +0 -901
  104. data/vendor/crates/spikard-http/src/handler_trait.rs +0 -1015
  105. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +0 -290
  106. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +0 -502
  107. data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +0 -648
  108. data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +0 -58
  109. data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +0 -1207
  110. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +0 -2262
  111. data/vendor/crates/spikard-http/src/lib.rs +0 -548
  112. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -230
  113. data/vendor/crates/spikard-http/src/lifecycle.rs +0 -1193
  114. data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -560
  115. data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -912
  116. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -513
  117. data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -768
  118. data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
  119. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -535
  120. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -1363
  121. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -667
  122. data/vendor/crates/spikard-http/src/query_parser.rs +0 -793
  123. data/vendor/crates/spikard-http/src/response.rs +0 -720
  124. data/vendor/crates/spikard-http/src/server/fast_router.rs +0 -186
  125. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +0 -858
  126. data/vendor/crates/spikard-http/src/server/handler.rs +0 -1661
  127. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -253
  128. data/vendor/crates/spikard-http/src/server/mod.rs +0 -1649
  129. data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -871
  130. data/vendor/crates/spikard-http/src/server/routing_factory.rs +0 -618
  131. data/vendor/crates/spikard-http/src/sse.rs +0 -1409
  132. data/vendor/crates/spikard-http/src/testing/form.rs +0 -52
  133. data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -64
  134. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -787
  135. data/vendor/crates/spikard-http/src/testing.rs +0 -617
  136. data/vendor/crates/spikard-http/src/websocket.rs +0 -1477
  137. data/vendor/crates/spikard-http/tests/auth_integration.rs +0 -645
  138. data/vendor/crates/spikard-http/tests/background_behavior.rs +0 -832
  139. data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +0 -1012
  140. data/vendor/crates/spikard-http/tests/common/handlers.rs +0 -309
  141. data/vendor/crates/spikard-http/tests/common/mod.rs +0 -33
  142. data/vendor/crates/spikard-http/tests/common/test_builders.rs +0 -628
  143. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +0 -162
  144. data/vendor/crates/spikard-http/tests/di_integration.rs +0 -192
  145. data/vendor/crates/spikard-http/tests/doc_snippets.rs +0 -5
  146. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +0 -430
  147. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +0 -738
  148. data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +0 -652
  149. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +0 -334
  150. data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +0 -532
  151. data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +0 -495
  152. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +0 -974
  153. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +0 -1093
  154. data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +0 -389
  155. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +0 -656
  156. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +0 -513
  157. data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +0 -328
  158. data/vendor/crates/spikard-http/tests/server_config_builder.rs +0 -314
  159. data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +0 -200
  160. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +0 -83
  161. data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +0 -464
  162. data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +0 -286
  163. data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +0 -118
  164. data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +0 -99
  165. data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +0 -204
  166. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +0 -421
  167. data/vendor/crates/spikard-http/tests/server_router_behavior.rs +0 -121
  168. data/vendor/crates/spikard-http/tests/sse_behavior.rs +0 -620
  169. data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +0 -584
  170. data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +0 -130
  171. data/vendor/crates/spikard-http/tests/test_client_requests.rs +0 -167
  172. data/vendor/crates/spikard-http/tests/testing_helpers.rs +0 -87
  173. data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +0 -155
  174. data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +0 -82
  175. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +0 -663
  176. data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +0 -440
  177. data/vendor/crates/spikard-http/tests/websocket_integration.rs +0 -150
  178. data/vendor/crates/spikard-rb/Cargo.toml +0 -68
  179. data/vendor/crates/spikard-rb/build.rs +0 -200
  180. data/vendor/crates/spikard-rb/src/background.rs +0 -63
  181. data/vendor/crates/spikard-rb/src/config/mod.rs +0 -5
  182. data/vendor/crates/spikard-rb/src/config/server_config.rs +0 -401
  183. data/vendor/crates/spikard-rb/src/conversion.rs +0 -688
  184. data/vendor/crates/spikard-rb/src/di/builder.rs +0 -100
  185. data/vendor/crates/spikard-rb/src/di/mod.rs +0 -375
  186. data/vendor/crates/spikard-rb/src/grpc/handler.rs +0 -834
  187. data/vendor/crates/spikard-rb/src/grpc/mod.rs +0 -13
  188. data/vendor/crates/spikard-rb/src/gvl.rs +0 -80
  189. data/vendor/crates/spikard-rb/src/handler.rs +0 -699
  190. data/vendor/crates/spikard-rb/src/integration/mod.rs +0 -3
  191. data/vendor/crates/spikard-rb/src/lib.rs +0 -2264
  192. data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -303
  193. data/vendor/crates/spikard-rb/src/metadata/mod.rs +0 -5
  194. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +0 -507
  195. data/vendor/crates/spikard-rb/src/request.rs +0 -439
  196. data/vendor/crates/spikard-rb/src/runtime/mod.rs +0 -5
  197. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +0 -344
  198. data/vendor/crates/spikard-rb/src/server.rs +0 -307
  199. data/vendor/crates/spikard-rb/src/sse.rs +0 -231
  200. data/vendor/crates/spikard-rb/src/testing/client.rs +0 -698
  201. data/vendor/crates/spikard-rb/src/testing/mod.rs +0 -7
  202. data/vendor/crates/spikard-rb/src/testing/sse.rs +0 -108
  203. data/vendor/crates/spikard-rb/src/testing/websocket.rs +0 -573
  204. data/vendor/crates/spikard-rb/src/websocket.rs +0 -475
  205. data/vendor/crates/spikard-rb-macros/Cargo.toml +0 -25
  206. data/vendor/crates/spikard-rb-macros/src/lib.rs +0 -51
@@ -1,506 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spikard
4
- # Compression configuration for response compression middleware.
5
- #
6
- # Spikard supports gzip and brotli compression for responses.
7
- # Compression is applied based on Accept-Encoding headers.
8
- #
9
- # @example
10
- # compression = CompressionConfig.new(
11
- # gzip: true,
12
- # brotli: true,
13
- # min_size: 1024,
14
- # quality: 6
15
- # )
16
- class CompressionConfig
17
- attr_accessor :gzip, :brotli, :min_size, :quality
18
-
19
- # @param gzip [Boolean] Enable gzip compression (default: true)
20
- # @param brotli [Boolean] Enable brotli compression (default: true)
21
- # @param min_size [Integer] Minimum response size in bytes to compress (default: 1024)
22
- # @param quality [Integer] Compression quality level (0-11 for brotli, 0-9 for gzip, default: 6)
23
- def initialize(gzip: true, brotli: true, min_size: 1024, quality: 6)
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'
52
- end
53
- end
54
-
55
- # Rate limiting configuration using Generic Cell Rate Algorithm (GCRA).
56
- #
57
- # By default, rate limits are applied per IP address.
58
- #
59
- # @example
60
- # rate_limit = RateLimitConfig.new(
61
- # per_second: 100,
62
- # burst: 200,
63
- # ip_based: true
64
- # )
65
- class RateLimitConfig
66
- attr_accessor :per_second, :burst, :ip_based
67
-
68
- # @param per_second [Integer] Maximum requests per second
69
- # @param burst [Integer] Burst allowance - allows temporary spikes
70
- # @param ip_based [Boolean] Apply rate limits per IP address (default: true)
71
- def initialize(per_second:, burst:, ip_based: true)
72
- @per_second = per_second
73
- @burst = burst
74
- @ip_based = ip_based
75
- end
76
- end
77
-
78
- # JWT authentication configuration.
79
- #
80
- # Validates JWT tokens using the specified secret and algorithm.
81
- # Tokens are expected in the Authorization header as "Bearer <token>".
82
- #
83
- # Supported algorithms:
84
- # - HS256, HS384, HS512 (HMAC with SHA)
85
- # - RS256, RS384, RS512 (RSA signatures)
86
- # - ES256, ES384, ES512 (ECDSA signatures)
87
- # - PS256, PS384, PS512 (RSA-PSS signatures)
88
- #
89
- # @example
90
- # jwt = JwtConfig.new(
91
- # secret: 'your-secret-key',
92
- # algorithm: 'HS256',
93
- # audience: ['api.example.com'],
94
- # issuer: 'auth.example.com',
95
- # leeway: 30
96
- # )
97
- class JwtConfig
98
- attr_accessor :secret, :algorithm, :audience, :issuer, :leeway
99
-
100
- # @param secret [String] Secret key for JWT validation
101
- # @param algorithm [String] JWT algorithm (default: "HS256")
102
- # @param audience [Array<String>, nil] Expected audience claim(s)
103
- # @param issuer [String, nil] Expected issuer claim
104
- # @param leeway [Integer] Time leeway in seconds for exp/nbf/iat claims (default: 0)
105
- def initialize(secret:, algorithm: 'HS256', audience: nil, issuer: nil, leeway: 0)
106
- @secret = secret
107
- @algorithm = algorithm
108
- @audience = audience
109
- @issuer = issuer
110
- @leeway = leeway
111
- end
112
- end
113
-
114
- # API key authentication configuration.
115
- #
116
- # Validates API keys from request headers. Keys are matched exactly.
117
- #
118
- # @example
119
- # api_key = ApiKeyConfig.new(
120
- # keys: ['key-1', 'key-2', 'key-3'],
121
- # header_name: 'X-API-Key'
122
- # )
123
- class ApiKeyConfig
124
- attr_accessor :keys, :header_name
125
-
126
- # @param keys [Array<String>] List of valid API keys
127
- # @param header_name [String] HTTP header name to check for API key (default: "X-API-Key")
128
- def initialize(keys:, header_name: 'X-API-Key')
129
- @keys = keys
130
- @header_name = header_name
131
- end
132
- end
133
-
134
- # Static file serving configuration.
135
- #
136
- # Serves files from a directory at a given route prefix.
137
- # Multiple static file configurations can be registered.
138
- #
139
- # @example
140
- # static = StaticFilesConfig.new(
141
- # directory: './public',
142
- # route_prefix: '/static',
143
- # index_file: true,
144
- # cache_control: 'public, max-age=3600'
145
- # )
146
- class StaticFilesConfig
147
- attr_accessor :directory, :route_prefix, :index_file, :cache_control
148
-
149
- # @param directory [String] Directory path containing static files
150
- # @param route_prefix [String] URL prefix for serving static files (e.g., "/static")
151
- # @param index_file [Boolean] Serve index.html for directory requests (default: true)
152
- # @param cache_control [String, nil] Optional Cache-Control header value (e.g., "public, max-age=3600")
153
- def initialize(directory:, route_prefix:, index_file: true, cache_control: nil)
154
- @directory = directory
155
- @route_prefix = route_prefix
156
- @index_file = index_file
157
- @cache_control = cache_control
158
- end
159
- end
160
-
161
- # Contact information for OpenAPI documentation.
162
- #
163
- # @example
164
- # contact = ContactInfo.new(
165
- # name: 'API Team',
166
- # email: 'api@example.com',
167
- # url: 'https://example.com'
168
- # )
169
- class ContactInfo
170
- attr_accessor :name, :email, :url
171
-
172
- # @param name [String, nil] Name of the contact person/organization
173
- # @param email [String, nil] Email address for contact
174
- # @param url [String, nil] URL for contact information
175
- def initialize(name: nil, email: nil, url: nil)
176
- @name = name
177
- @email = email
178
- @url = url
179
- end
180
- end
181
-
182
- # License information for OpenAPI documentation.
183
- #
184
- # @example
185
- # license = LicenseInfo.new(
186
- # name: 'MIT',
187
- # url: 'https://opensource.org/licenses/MIT'
188
- # )
189
- class LicenseInfo
190
- attr_accessor :name, :url
191
-
192
- # @param name [String] License name (e.g., "MIT", "Apache 2.0")
193
- # @param url [String, nil] URL to the full license text
194
- def initialize(name:, url: nil)
195
- @name = name
196
- @url = url
197
- end
198
- end
199
-
200
- # Server information for OpenAPI documentation.
201
- #
202
- # Multiple servers can be specified for different environments.
203
- #
204
- # @example
205
- # server = ServerInfo.new(
206
- # url: 'https://api.example.com',
207
- # description: 'Production'
208
- # )
209
- class ServerInfo
210
- attr_accessor :url, :description
211
-
212
- # @param url [String] Server URL (e.g., "https://api.example.com")
213
- # @param description [String, nil] Description of the server (e.g., "Production", "Staging")
214
- def initialize(url:, description: nil)
215
- @url = url
216
- @description = description
217
- end
218
- end
219
-
220
- # Security scheme configuration for OpenAPI documentation.
221
- #
222
- # Supports HTTP (Bearer/JWT) and API Key authentication schemes.
223
- #
224
- # @example HTTP Bearer
225
- # scheme = SecuritySchemeInfo.new(
226
- # type: 'http',
227
- # scheme: 'bearer',
228
- # bearer_format: 'JWT'
229
- # )
230
- #
231
- # @example API Key
232
- # scheme = SecuritySchemeInfo.new(
233
- # type: 'apiKey',
234
- # location: 'header',
235
- # name: 'X-API-Key'
236
- # )
237
- class SecuritySchemeInfo
238
- attr_accessor :type, :scheme, :bearer_format, :location, :name
239
-
240
- # @param type [String] Security scheme type ("http" or "apiKey")
241
- # @param scheme [String, nil] HTTP scheme (e.g., "bearer", "basic") - for type="http"
242
- # @param bearer_format [String, nil] Format hint for Bearer tokens (e.g., "JWT") - for type="http"
243
- # @param location [String, nil] Where to look for the API key ("header", "query", or "cookie") - for type="apiKey"
244
- # @param name [String, nil] Parameter name (e.g., "X-API-Key") - for type="apiKey"
245
- def initialize(type:, scheme: nil, bearer_format: nil, location: nil, name: nil)
246
- @type = type
247
- @scheme = scheme
248
- @bearer_format = bearer_format
249
- @location = location
250
- @name = name
251
-
252
- validate!
253
- end
254
-
255
- private
256
-
257
- def validate!
258
- case @type
259
- when 'http'
260
- raise ArgumentError, 'scheme is required for type="http"' if @scheme.nil?
261
- when 'apiKey'
262
- raise ArgumentError, 'location and name are required for type="apiKey"' if @location.nil? || @name.nil?
263
- else
264
- raise ArgumentError, "type must be 'http' or 'apiKey', got: #{@type.inspect}"
265
- end
266
- end
267
- end
268
-
269
- # OpenAPI 3.1.0 documentation configuration.
270
- #
271
- # Spikard can automatically generate OpenAPI documentation from your routes.
272
- # When enabled, it serves:
273
- # - Swagger UI at /docs (customizable)
274
- # - Redoc at /redoc (customizable)
275
- # - OpenAPI JSON spec at /openapi.json (customizable)
276
- #
277
- # Security schemes are auto-detected from middleware configuration.
278
- # Schemas are generated from your route type hints and validation.
279
- #
280
- # @example
281
- # openapi = OpenApiConfig.new(
282
- # enabled: true,
283
- # title: 'My API',
284
- # version: '1.0.0',
285
- # description: 'A great API built with Spikard',
286
- # contact: ContactInfo.new(
287
- # name: 'API Team',
288
- # email: 'api@example.com',
289
- # url: 'https://example.com'
290
- # ),
291
- # license: LicenseInfo.new(
292
- # name: 'MIT',
293
- # url: 'https://opensource.org/licenses/MIT'
294
- # ),
295
- # servers: [
296
- # ServerInfo.new(url: 'https://api.example.com', description: 'Production'),
297
- # ServerInfo.new(url: 'http://localhost:8000', description: 'Development')
298
- # ]
299
- # )
300
- class OpenApiConfig
301
- attr_accessor :enabled, :title, :version, :description,
302
- :swagger_ui_path, :redoc_path, :openapi_json_path,
303
- :contact, :license, :servers, :security_schemes
304
-
305
- # @param enabled [Boolean] Enable OpenAPI generation (default: false for zero overhead)
306
- # @param title [String] API title (default: "API")
307
- # @param version [String] API version (default: "1.0.0")
308
- # @param description [String, nil] API description (supports Markdown)
309
- # @param swagger_ui_path [String] Path to serve Swagger UI (default: "/docs")
310
- # @param redoc_path [String] Path to serve Redoc (default: "/redoc")
311
- # @param openapi_json_path [String] Path to serve OpenAPI JSON spec (default: "/openapi.json")
312
- # @param contact [ContactInfo, nil] Contact information for the API
313
- # @param license [LicenseInfo, nil] License information for the API
314
- # @param servers [Array<ServerInfo>] List of server URLs for different environments (default: [])
315
- # @param security_schemes [Hash<String, SecuritySchemeInfo>] Custom security schemes (auto-detected if not provided)
316
- def initialize(
317
- enabled: false,
318
- title: 'API',
319
- version: '1.0.0',
320
- description: nil,
321
- swagger_ui_path: '/docs',
322
- redoc_path: '/redoc',
323
- openapi_json_path: '/openapi.json',
324
- contact: nil,
325
- license: nil,
326
- servers: [],
327
- security_schemes: {}
328
- )
329
- @enabled = enabled
330
- @title = title
331
- @version = version
332
- @description = description
333
- @swagger_ui_path = swagger_ui_path
334
- @redoc_path = redoc_path
335
- @openapi_json_path = openapi_json_path
336
- @contact = contact
337
- @license = license
338
- @servers = servers
339
- @security_schemes = security_schemes
340
- end
341
- end
342
-
343
- # JSON-RPC endpoint configuration.
344
- class JsonRpcConfig
345
- attr_accessor :enabled, :endpoint_path, :enable_batch, :max_batch_size
346
-
347
- # @param enabled [Boolean] Enable JSON-RPC endpoint registration (default: true)
348
- # @param endpoint_path [String] JSON-RPC endpoint path (default: "/rpc")
349
- # @param enable_batch [Boolean] Enable JSON-RPC batch support (default: true)
350
- # @param max_batch_size [Integer] Maximum batch size (default: 100)
351
- def initialize(enabled: true, endpoint_path: '/rpc', enable_batch: true, max_batch_size: 100)
352
- @enabled = normalize_boolean('enabled', enabled)
353
- @endpoint_path = endpoint_path
354
- @enable_batch = normalize_boolean('enable_batch', enable_batch)
355
- @max_batch_size = normalize_positive_integer('max_batch_size', max_batch_size)
356
- end
357
-
358
- private
359
-
360
- def normalize_boolean(name, value)
361
- return value if [true, false].include?(value)
362
-
363
- raise ArgumentError, "#{name} must be a boolean"
364
- end
365
-
366
- def normalize_positive_integer(name, value)
367
- raise ArgumentError, "#{name} must be an Integer" unless value.is_a?(Integer)
368
- return value if value.positive?
369
-
370
- raise ArgumentError, "#{name} must be > 0"
371
- end
372
- end
373
-
374
- # Complete server configuration for Spikard.
375
- #
376
- # This is the main configuration object that controls all aspects of the server
377
- # including network settings, middleware, authentication, and more.
378
- #
379
- # @example
380
- # config = ServerConfig.new(
381
- # host: '0.0.0.0',
382
- # port: 8080,
383
- # workers: 4,
384
- # compression: CompressionConfig.new(quality: 9),
385
- # rate_limit: RateLimitConfig.new(per_second: 100, burst: 200),
386
- # static_files: [
387
- # StaticFilesConfig.new(
388
- # directory: './public',
389
- # route_prefix: '/static'
390
- # )
391
- # ],
392
- # openapi: OpenApiConfig.new(
393
- # enabled: true,
394
- # title: 'My API',
395
- # version: '1.0.0'
396
- # )
397
- # )
398
- class ServerConfig
399
- attr_accessor :host, :port, :workers,
400
- :enable_request_id, :max_body_size, :request_timeout,
401
- :compression, :rate_limit, :jwt_auth, :api_key_auth,
402
- :static_files, :graceful_shutdown, :shutdown_timeout,
403
- :openapi, :jsonrpc
404
-
405
- # @param host [String] Host address to bind to (default: "127.0.0.1")
406
- # @param port [Integer] Port number to listen on (default: 8000, range: 1-65535)
407
- # @param workers [Integer] Number of worker processes (default: 1)
408
- # @param enable_request_id [Boolean] Add X-Request-ID header to responses (default: true)
409
- # @param max_body_size [Integer, nil] Maximum request body size in bytes (default: 10MB, nil for unlimited)
410
- # @param request_timeout [Integer, nil] Request timeout in seconds (default: 30, nil for no timeout)
411
- # @param compression [CompressionConfig, nil] Response compression configuration (default: enabled with defaults)
412
- # @param rate_limit [RateLimitConfig, nil] Rate limiting configuration (default: nil/disabled)
413
- # @param jwt_auth [JwtConfig, nil] JWT authentication configuration (default: nil/disabled)
414
- # @param api_key_auth [ApiKeyConfig, nil] API key authentication configuration (default: nil/disabled)
415
- # @param static_files [Array<StaticFilesConfig>] List of static file serving configurations (default: [])
416
- # @param graceful_shutdown [Boolean] Enable graceful shutdown (default: true)
417
- # @param shutdown_timeout [Integer] Graceful shutdown timeout in seconds (default: 30)
418
- # @param openapi [OpenApiConfig, nil] OpenAPI configuration (default: nil/disabled)
419
- # @param jsonrpc [JsonRpcConfig, nil] JSON-RPC configuration (default: nil/disabled)
420
- def initialize(
421
- host: '127.0.0.1',
422
- port: 8000,
423
- workers: 1,
424
- enable_request_id: true,
425
- max_body_size: 10 * 1024 * 1024, # 10MB
426
- request_timeout: 30,
427
- compression: CompressionConfig.new,
428
- rate_limit: nil,
429
- jwt_auth: nil,
430
- api_key_auth: nil,
431
- static_files: [],
432
- graceful_shutdown: true,
433
- shutdown_timeout: 30,
434
- openapi: nil,
435
- jsonrpc: nil
436
- )
437
- @host = host
438
- @port = normalize_port(port)
439
- @workers = normalize_workers(workers)
440
- @enable_request_id = normalize_boolean('enable_request_id', enable_request_id)
441
- @max_body_size = normalize_optional_nonnegative_integer('max_body_size', max_body_size)
442
- @request_timeout = normalize_timeout('request_timeout', request_timeout)
443
- @compression = compression
444
- @rate_limit = rate_limit
445
- @jwt_auth = jwt_auth
446
- @api_key_auth = api_key_auth
447
- @static_files = normalize_static_files(static_files)
448
- @graceful_shutdown = normalize_boolean('graceful_shutdown', graceful_shutdown)
449
- @shutdown_timeout = normalize_timeout('shutdown_timeout', shutdown_timeout)
450
- @openapi = openapi
451
- @jsonrpc = jsonrpc
452
- end
453
-
454
- private
455
-
456
- def normalize_port(port)
457
- raise ArgumentError, 'port must be an Integer' unless port.is_a?(Integer)
458
- return port if port.between?(1, 65_535)
459
-
460
- raise ArgumentError, 'port must be between 1 and 65535'
461
- end
462
-
463
- def normalize_workers(workers)
464
- raise ArgumentError, 'workers must be an Integer' unless workers.is_a?(Integer)
465
- return workers if workers >= 1
466
-
467
- raise ArgumentError, 'workers must be >= 1'
468
- end
469
-
470
- def normalize_boolean(name, value)
471
- return value if [true, false].include?(value)
472
-
473
- raise ArgumentError, "#{name} must be a boolean"
474
- end
475
-
476
- def normalize_optional_nonnegative_integer(name, value)
477
- return nil if value.nil?
478
- raise ArgumentError, "#{name} must be an Integer" unless value.is_a?(Integer)
479
- return value if value >= 0
480
-
481
- raise ArgumentError, "#{name} must be >= 0"
482
- end
483
-
484
- def normalize_timeout(name, value)
485
- return nil if value.nil?
486
- raise ArgumentError, "#{name} must be a number" unless value.is_a?(Integer) || value.is_a?(Float)
487
-
488
- normalized = value.to_i
489
- return normalized if normalized >= 0
490
-
491
- raise ArgumentError, "#{name} must be >= 0"
492
- end
493
-
494
- def normalize_static_files(static_files)
495
- return [] if static_files.nil?
496
- raise ArgumentError, 'static_files must be an Array' unless static_files.is_a?(Array)
497
-
498
- static_files.each do |entry|
499
- next if entry.is_a?(StaticFilesConfig)
500
-
501
- raise ArgumentError, 'static_files entries must be StaticFilesConfig'
502
- end
503
- static_files
504
- end
505
- end
506
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spikard
4
- # Conversion helpers between native Rust values and Ruby types.
5
- module Converters
6
- module_function
7
-
8
- # No-op conversion now that Rust materialises UploadFile.
9
- def convert_handler_body(body)
10
- body
11
- end
12
- end
13
- end
data/lib/spikard/grpc.rb DELETED
@@ -1,182 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spikard
4
- # gRPC support for Spikard
5
- #
6
- # This module provides Ruby bindings for handling gRPC requests through
7
- # Spikard's Rust-based gRPC runtime. Handlers receive protobuf messages
8
- # as binary strings and use the google-protobuf gem for serialization.
9
- #
10
- # @example Basic gRPC handler
11
- # require 'spikard/grpc'
12
- # require 'user_pb' # Generated protobuf
13
- #
14
- # class UserServiceHandler < Spikard::Grpc::Handler
15
- # def handle_request(request)
16
- # case request.method_name
17
- # when 'GetUser'
18
- # # Deserialize request
19
- # req = Example::GetUserRequest.decode(request.payload)
20
- #
21
- # # Process request
22
- # user = Example::User.new(id: req.id, name: 'John Doe')
23
- #
24
- # # Serialize response
25
- # Spikard::Grpc::Response.new(payload: Example::User.encode(user))
26
- # else
27
- # raise "Unknown method: #{request.method_name}"
28
- # end
29
- # end
30
- # end
31
- module Grpc
32
- # gRPC request object
33
- #
34
- # Represents an incoming gRPC request with service/method information
35
- # and a binary protobuf payload.
36
- #
37
- # @!attribute [r] service_name
38
- # @return [String] Fully qualified service name (e.g., "mypackage.MyService")
39
- # @!attribute [r] method_name
40
- # @return [String] Method name (e.g., "GetUser")
41
- # @!attribute [r] payload
42
- # @return [String] Binary string containing serialized protobuf message
43
- # @!attribute [r] metadata
44
- # @return [Hash<String, String>] gRPC metadata (headers)
45
- # rubocop:disable Lint/EmptyClass -- Implementation in Rust via FFI
46
- class Request
47
- # These methods are implemented in Rust via Magnus FFI.
48
- # See: crates/spikard-rb/src/grpc/handler.rs for implementation details.
49
- end
50
- # rubocop:enable Lint/EmptyClass
51
-
52
- # gRPC response object
53
- #
54
- # Used to return gRPC responses from handlers. The payload should be
55
- # a binary string containing a serialized protobuf message.
56
- #
57
- # @example Creating a response
58
- # user = Example::User.new(id: 1, name: 'Alice')
59
- # response = Spikard::Grpc::Response.new(payload: Example::User.encode(user))
60
- #
61
- # @example Adding metadata
62
- # response = Spikard::Grpc::Response.new(payload: encoded_message)
63
- # response.metadata = { 'x-custom-header' => 'value' }
64
- class Response
65
- # @!attribute [w] metadata
66
- # @return [Hash<String, String>] gRPC metadata to include in response
67
-
68
- # Create a new gRPC response
69
- #
70
- # @param payload [String] Binary string containing serialized protobuf message
71
- # @raise [ArgumentError] if payload is not a String
72
- #
73
- # Note: Implementation in Rust (Magnus FFI)
74
- # See: crates/spikard-rb/src/grpc/handler.rs
75
-
76
- # Create an error response
77
- #
78
- # @param message [String] Error message
79
- # @param metadata [Hash<String, String>] Optional gRPC metadata
80
- # @return [Response] A response with error status
81
- #
82
- # @example
83
- # response = Spikard::Grpc::Response.error('Method not implemented')
84
- def self.error(message, metadata = {})
85
- error_metadata = metadata.merge(
86
- 'grpc-status' => 'INTERNAL',
87
- 'grpc-message' => message
88
- )
89
- response = new(payload: '')
90
- response.metadata = error_metadata
91
- response
92
- end
93
- end
94
-
95
- # Base class for gRPC handlers
96
- #
97
- # Subclass this to implement gRPC service handlers. Override
98
- # {#handle_request} to process incoming requests.
99
- #
100
- # @example Implementing a handler
101
- # class MyServiceHandler < Spikard::Grpc::Handler
102
- # def handle_request(request)
103
- # case request.method_name
104
- # when 'MethodOne'
105
- # # Handle MethodOne
106
- # req = MyPackage::MethodOneRequest.decode(request.payload)
107
- # resp = MyPackage::MethodOneResponse.new(...)
108
- # Spikard::Grpc::Response.new(payload: MyPackage::MethodOneResponse.encode(resp))
109
- # when 'MethodTwo'
110
- # # Handle MethodTwo
111
- # # ...
112
- # else
113
- # raise "Unknown method: #{request.method_name}"
114
- # end
115
- # end
116
- # end
117
- class Handler
118
- # Handle a gRPC request
119
- #
120
- # This method must be overridden by subclasses to implement the
121
- # actual request handling logic.
122
- #
123
- # @param request [Spikard::Grpc::Request] The incoming gRPC request
124
- # @return [Spikard::Grpc::Response] The gRPC response
125
- # @raise [NotImplementedError] if not overridden by subclass
126
- def handle_request(request)
127
- raise NotImplementedError, "#{self.class}#handle_request must be implemented"
128
- end
129
- end
130
-
131
- # Service registry for gRPC handlers
132
- #
133
- # Manages registration and lookup of gRPC service handlers.
134
- # Handlers are registered by service name and method.
135
- #
136
- # @example Registering a handler
137
- # service = Spikard::Grpc::Service.new
138
- # handler = UserServiceHandler.new
139
- # service.register_handler('mypackage.UserService', handler)
140
- class Service
141
- def initialize
142
- @handlers = {}
143
- end
144
-
145
- # Register a gRPC handler for a service
146
- #
147
- # @param service_name [String] Fully qualified service name
148
- # @param handler [Spikard::Grpc::Handler] Handler instance
149
- # @raise [ArgumentError] if service_name is invalid or handler doesn't respond to handle_request
150
- def register_handler(service_name, handler)
151
- raise ArgumentError, 'Service name cannot be empty' if service_name.nil? || service_name.empty?
152
-
153
- raise ArgumentError, 'Handler must respond to :handle_request' unless handler.respond_to?(:handle_request)
154
-
155
- @handlers[service_name] = handler
156
- end
157
-
158
- # Get a handler by service name
159
- #
160
- # @param service_name [String] Fully qualified service name
161
- # @return [Spikard::Grpc::Handler, nil] The handler or nil if not found
162
- def get_handler(service_name)
163
- @handlers[service_name]
164
- end
165
-
166
- # Get all registered service names
167
- #
168
- # @return [Array<String>] List of registered service names
169
- def service_names
170
- @handlers.keys
171
- end
172
-
173
- # Check if a service is registered
174
- #
175
- # @param service_name [String] Fully qualified service name
176
- # @return [Boolean] true if the service is registered
177
- def registered?(service_name)
178
- @handlers.key?(service_name)
179
- end
180
- end
181
- end
182
- end