spikard 0.2.5 → 0.3.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +659 -626
  4. data/ext/spikard_rb/Cargo.toml +17 -17
  5. data/ext/spikard_rb/extconf.rb +10 -10
  6. data/ext/spikard_rb/src/lib.rs +6 -6
  7. data/lib/spikard/app.rb +386 -374
  8. data/lib/spikard/background.rb +27 -27
  9. data/lib/spikard/config.rb +396 -396
  10. data/lib/spikard/converters.rb +13 -85
  11. data/lib/spikard/handler_wrapper.rb +113 -116
  12. data/lib/spikard/provide.rb +214 -228
  13. data/lib/spikard/response.rb +173 -109
  14. data/lib/spikard/schema.rb +243 -243
  15. data/lib/spikard/sse.rb +111 -111
  16. data/lib/spikard/streaming_response.rb +44 -21
  17. data/lib/spikard/testing.rb +221 -221
  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 +360 -349
  23. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +2 -0
  24. metadata +5 -85
  25. data/vendor/crates/spikard-core/Cargo.toml +0 -40
  26. data/vendor/crates/spikard-core/src/bindings/mod.rs +0 -3
  27. data/vendor/crates/spikard-core/src/bindings/response.rs +0 -133
  28. data/vendor/crates/spikard-core/src/debug.rs +0 -63
  29. data/vendor/crates/spikard-core/src/di/container.rs +0 -726
  30. data/vendor/crates/spikard-core/src/di/dependency.rs +0 -273
  31. data/vendor/crates/spikard-core/src/di/error.rs +0 -118
  32. data/vendor/crates/spikard-core/src/di/factory.rs +0 -538
  33. data/vendor/crates/spikard-core/src/di/graph.rs +0 -545
  34. data/vendor/crates/spikard-core/src/di/mod.rs +0 -192
  35. data/vendor/crates/spikard-core/src/di/resolved.rs +0 -411
  36. data/vendor/crates/spikard-core/src/di/value.rs +0 -283
  37. data/vendor/crates/spikard-core/src/http.rs +0 -153
  38. data/vendor/crates/spikard-core/src/lib.rs +0 -28
  39. data/vendor/crates/spikard-core/src/lifecycle.rs +0 -422
  40. data/vendor/crates/spikard-core/src/parameters.rs +0 -719
  41. data/vendor/crates/spikard-core/src/problem.rs +0 -310
  42. data/vendor/crates/spikard-core/src/request_data.rs +0 -189
  43. data/vendor/crates/spikard-core/src/router.rs +0 -249
  44. data/vendor/crates/spikard-core/src/schema_registry.rs +0 -183
  45. data/vendor/crates/spikard-core/src/type_hints.rs +0 -304
  46. data/vendor/crates/spikard-core/src/validation.rs +0 -699
  47. data/vendor/crates/spikard-http/Cargo.toml +0 -58
  48. data/vendor/crates/spikard-http/src/auth.rs +0 -247
  49. data/vendor/crates/spikard-http/src/background.rs +0 -249
  50. data/vendor/crates/spikard-http/src/bindings/mod.rs +0 -3
  51. data/vendor/crates/spikard-http/src/bindings/response.rs +0 -1
  52. data/vendor/crates/spikard-http/src/body_metadata.rs +0 -8
  53. data/vendor/crates/spikard-http/src/cors.rs +0 -490
  54. data/vendor/crates/spikard-http/src/debug.rs +0 -63
  55. data/vendor/crates/spikard-http/src/di_handler.rs +0 -423
  56. data/vendor/crates/spikard-http/src/handler_response.rs +0 -190
  57. data/vendor/crates/spikard-http/src/handler_trait.rs +0 -228
  58. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +0 -284
  59. data/vendor/crates/spikard-http/src/lib.rs +0 -529
  60. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -149
  61. data/vendor/crates/spikard-http/src/lifecycle.rs +0 -428
  62. data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -285
  63. data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -86
  64. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -147
  65. data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -287
  66. data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
  67. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -190
  68. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -308
  69. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -195
  70. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  71. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  72. data/vendor/crates/spikard-http/src/query_parser.rs +0 -369
  73. data/vendor/crates/spikard-http/src/response.rs +0 -399
  74. data/vendor/crates/spikard-http/src/router.rs +0 -1
  75. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  76. data/vendor/crates/spikard-http/src/server/handler.rs +0 -80
  77. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -98
  78. data/vendor/crates/spikard-http/src/server/mod.rs +0 -805
  79. data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -119
  80. data/vendor/crates/spikard-http/src/sse.rs +0 -447
  81. data/vendor/crates/spikard-http/src/testing/form.rs +0 -14
  82. data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -60
  83. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -285
  84. data/vendor/crates/spikard-http/src/testing.rs +0 -377
  85. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  86. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  87. data/vendor/crates/spikard-http/src/websocket.rs +0 -324
  88. data/vendor/crates/spikard-rb/Cargo.toml +0 -42
  89. data/vendor/crates/spikard-rb/build.rs +0 -8
  90. data/vendor/crates/spikard-rb/src/background.rs +0 -63
  91. data/vendor/crates/spikard-rb/src/config.rs +0 -294
  92. data/vendor/crates/spikard-rb/src/conversion.rs +0 -392
  93. data/vendor/crates/spikard-rb/src/di.rs +0 -409
  94. data/vendor/crates/spikard-rb/src/handler.rs +0 -534
  95. data/vendor/crates/spikard-rb/src/lib.rs +0 -2020
  96. data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -267
  97. data/vendor/crates/spikard-rb/src/server.rs +0 -283
  98. data/vendor/crates/spikard-rb/src/sse.rs +0 -231
  99. data/vendor/crates/spikard-rb/src/test_client.rs +0 -404
  100. data/vendor/crates/spikard-rb/src/test_sse.rs +0 -143
  101. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  102. data/vendor/crates/spikard-rb/src/websocket.rs +0 -233
  103. /data/vendor/bundle/ruby/{3.3.0 → 3.4.0}/gems/diff-lcs-1.6.2/mise.toml +0 -0
@@ -1,396 +1,396 @@
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 = gzip
25
- @brotli = brotli
26
- @min_size = min_size
27
- @quality = quality
28
- end
29
- end
30
-
31
- # Rate limiting configuration using Generic Cell Rate Algorithm (GCRA).
32
- #
33
- # By default, rate limits are applied per IP address.
34
- #
35
- # @example
36
- # rate_limit = RateLimitConfig.new(
37
- # per_second: 100,
38
- # burst: 200,
39
- # ip_based: true
40
- # )
41
- class RateLimitConfig
42
- attr_accessor :per_second, :burst, :ip_based
43
-
44
- # @param per_second [Integer] Maximum requests per second
45
- # @param burst [Integer] Burst allowance - allows temporary spikes
46
- # @param ip_based [Boolean] Apply rate limits per IP address (default: true)
47
- def initialize(per_second:, burst:, ip_based: true)
48
- @per_second = per_second
49
- @burst = burst
50
- @ip_based = ip_based
51
- end
52
- end
53
-
54
- # JWT authentication configuration.
55
- #
56
- # Validates JWT tokens using the specified secret and algorithm.
57
- # Tokens are expected in the Authorization header as "Bearer <token>".
58
- #
59
- # Supported algorithms:
60
- # - HS256, HS384, HS512 (HMAC with SHA)
61
- # - RS256, RS384, RS512 (RSA signatures)
62
- # - ES256, ES384, ES512 (ECDSA signatures)
63
- # - PS256, PS384, PS512 (RSA-PSS signatures)
64
- #
65
- # @example
66
- # jwt = JwtConfig.new(
67
- # secret: 'your-secret-key',
68
- # algorithm: 'HS256',
69
- # audience: ['api.example.com'],
70
- # issuer: 'auth.example.com',
71
- # leeway: 30
72
- # )
73
- class JwtConfig
74
- attr_accessor :secret, :algorithm, :audience, :issuer, :leeway
75
-
76
- # @param secret [String] Secret key for JWT validation
77
- # @param algorithm [String] JWT algorithm (default: "HS256")
78
- # @param audience [Array<String>, nil] Expected audience claim(s)
79
- # @param issuer [String, nil] Expected issuer claim
80
- # @param leeway [Integer] Time leeway in seconds for exp/nbf/iat claims (default: 0)
81
- def initialize(secret:, algorithm: 'HS256', audience: nil, issuer: nil, leeway: 0)
82
- @secret = secret
83
- @algorithm = algorithm
84
- @audience = audience
85
- @issuer = issuer
86
- @leeway = leeway
87
- end
88
- end
89
-
90
- # API key authentication configuration.
91
- #
92
- # Validates API keys from request headers. Keys are matched exactly.
93
- #
94
- # @example
95
- # api_key = ApiKeyConfig.new(
96
- # keys: ['key-1', 'key-2', 'key-3'],
97
- # header_name: 'X-API-Key'
98
- # )
99
- class ApiKeyConfig
100
- attr_accessor :keys, :header_name
101
-
102
- # @param keys [Array<String>] List of valid API keys
103
- # @param header_name [String] HTTP header name to check for API key (default: "X-API-Key")
104
- def initialize(keys:, header_name: 'X-API-Key')
105
- @keys = keys
106
- @header_name = header_name
107
- end
108
- end
109
-
110
- # Static file serving configuration.
111
- #
112
- # Serves files from a directory at a given route prefix.
113
- # Multiple static file configurations can be registered.
114
- #
115
- # @example
116
- # static = StaticFilesConfig.new(
117
- # directory: './public',
118
- # route_prefix: '/static',
119
- # index_file: true,
120
- # cache_control: 'public, max-age=3600'
121
- # )
122
- class StaticFilesConfig
123
- attr_accessor :directory, :route_prefix, :index_file, :cache_control
124
-
125
- # @param directory [String] Directory path containing static files
126
- # @param route_prefix [String] URL prefix for serving static files (e.g., "/static")
127
- # @param index_file [Boolean] Serve index.html for directory requests (default: true)
128
- # @param cache_control [String, nil] Optional Cache-Control header value (e.g., "public, max-age=3600")
129
- def initialize(directory:, route_prefix:, index_file: true, cache_control: nil)
130
- @directory = directory
131
- @route_prefix = route_prefix
132
- @index_file = index_file
133
- @cache_control = cache_control
134
- end
135
- end
136
-
137
- # Contact information for OpenAPI documentation.
138
- #
139
- # @example
140
- # contact = ContactInfo.new(
141
- # name: 'API Team',
142
- # email: 'api@example.com',
143
- # url: 'https://example.com'
144
- # )
145
- class ContactInfo
146
- attr_accessor :name, :email, :url
147
-
148
- # @param name [String, nil] Name of the contact person/organization
149
- # @param email [String, nil] Email address for contact
150
- # @param url [String, nil] URL for contact information
151
- def initialize(name: nil, email: nil, url: nil)
152
- @name = name
153
- @email = email
154
- @url = url
155
- end
156
- end
157
-
158
- # License information for OpenAPI documentation.
159
- #
160
- # @example
161
- # license = LicenseInfo.new(
162
- # name: 'MIT',
163
- # url: 'https://opensource.org/licenses/MIT'
164
- # )
165
- class LicenseInfo
166
- attr_accessor :name, :url
167
-
168
- # @param name [String] License name (e.g., "MIT", "Apache 2.0")
169
- # @param url [String, nil] URL to the full license text
170
- def initialize(name:, url: nil)
171
- @name = name
172
- @url = url
173
- end
174
- end
175
-
176
- # Server information for OpenAPI documentation.
177
- #
178
- # Multiple servers can be specified for different environments.
179
- #
180
- # @example
181
- # server = ServerInfo.new(
182
- # url: 'https://api.example.com',
183
- # description: 'Production'
184
- # )
185
- class ServerInfo
186
- attr_accessor :url, :description
187
-
188
- # @param url [String] Server URL (e.g., "https://api.example.com")
189
- # @param description [String, nil] Description of the server (e.g., "Production", "Staging")
190
- def initialize(url:, description: nil)
191
- @url = url
192
- @description = description
193
- end
194
- end
195
-
196
- # Security scheme configuration for OpenAPI documentation.
197
- #
198
- # Supports HTTP (Bearer/JWT) and API Key authentication schemes.
199
- #
200
- # @example HTTP Bearer
201
- # scheme = SecuritySchemeInfo.new(
202
- # type: 'http',
203
- # scheme: 'bearer',
204
- # bearer_format: 'JWT'
205
- # )
206
- #
207
- # @example API Key
208
- # scheme = SecuritySchemeInfo.new(
209
- # type: 'apiKey',
210
- # location: 'header',
211
- # name: 'X-API-Key'
212
- # )
213
- class SecuritySchemeInfo
214
- attr_accessor :type, :scheme, :bearer_format, :location, :name
215
-
216
- # @param type [String] Security scheme type ("http" or "apiKey")
217
- # @param scheme [String, nil] HTTP scheme (e.g., "bearer", "basic") - for type="http"
218
- # @param bearer_format [String, nil] Format hint for Bearer tokens (e.g., "JWT") - for type="http"
219
- # @param location [String, nil] Where to look for the API key ("header", "query", or "cookie") - for type="apiKey"
220
- # @param name [String, nil] Parameter name (e.g., "X-API-Key") - for type="apiKey"
221
- def initialize(type:, scheme: nil, bearer_format: nil, location: nil, name: nil)
222
- @type = type
223
- @scheme = scheme
224
- @bearer_format = bearer_format
225
- @location = location
226
- @name = name
227
-
228
- validate!
229
- end
230
-
231
- private
232
-
233
- def validate!
234
- case @type
235
- when 'http'
236
- raise ArgumentError, 'scheme is required for type="http"' if @scheme.nil?
237
- when 'apiKey'
238
- raise ArgumentError, 'location and name are required for type="apiKey"' if @location.nil? || @name.nil?
239
- else
240
- raise ArgumentError, "type must be 'http' or 'apiKey', got: #{@type.inspect}"
241
- end
242
- end
243
- end
244
-
245
- # OpenAPI 3.1.0 documentation configuration.
246
- #
247
- # Spikard can automatically generate OpenAPI documentation from your routes.
248
- # When enabled, it serves:
249
- # - Swagger UI at /docs (customizable)
250
- # - Redoc at /redoc (customizable)
251
- # - OpenAPI JSON spec at /openapi.json (customizable)
252
- #
253
- # Security schemes are auto-detected from middleware configuration.
254
- # Schemas are generated from your route type hints and validation.
255
- #
256
- # @example
257
- # openapi = OpenApiConfig.new(
258
- # enabled: true,
259
- # title: 'My API',
260
- # version: '1.0.0',
261
- # description: 'A great API built with Spikard',
262
- # contact: ContactInfo.new(
263
- # name: 'API Team',
264
- # email: 'api@example.com',
265
- # url: 'https://example.com'
266
- # ),
267
- # license: LicenseInfo.new(
268
- # name: 'MIT',
269
- # url: 'https://opensource.org/licenses/MIT'
270
- # ),
271
- # servers: [
272
- # ServerInfo.new(url: 'https://api.example.com', description: 'Production'),
273
- # ServerInfo.new(url: 'http://localhost:8000', description: 'Development')
274
- # ]
275
- # )
276
- class OpenApiConfig
277
- attr_accessor :enabled, :title, :version, :description,
278
- :swagger_ui_path, :redoc_path, :openapi_json_path,
279
- :contact, :license, :servers, :security_schemes
280
-
281
- # @param enabled [Boolean] Enable OpenAPI generation (default: false for zero overhead)
282
- # @param title [String] API title (default: "API")
283
- # @param version [String] API version (default: "1.0.0")
284
- # @param description [String, nil] API description (supports Markdown)
285
- # @param swagger_ui_path [String] Path to serve Swagger UI (default: "/docs")
286
- # @param redoc_path [String] Path to serve Redoc (default: "/redoc")
287
- # @param openapi_json_path [String] Path to serve OpenAPI JSON spec (default: "/openapi.json")
288
- # @param contact [ContactInfo, nil] Contact information for the API
289
- # @param license [LicenseInfo, nil] License information for the API
290
- # @param servers [Array<ServerInfo>] List of server URLs for different environments (default: [])
291
- # @param security_schemes [Hash<String, SecuritySchemeInfo>] Custom security schemes (auto-detected if not provided)
292
- def initialize(
293
- enabled: false,
294
- title: 'API',
295
- version: '1.0.0',
296
- description: nil,
297
- swagger_ui_path: '/docs',
298
- redoc_path: '/redoc',
299
- openapi_json_path: '/openapi.json',
300
- contact: nil,
301
- license: nil,
302
- servers: [],
303
- security_schemes: {}
304
- )
305
- @enabled = enabled
306
- @title = title
307
- @version = version
308
- @description = description
309
- @swagger_ui_path = swagger_ui_path
310
- @redoc_path = redoc_path
311
- @openapi_json_path = openapi_json_path
312
- @contact = contact
313
- @license = license
314
- @servers = servers
315
- @security_schemes = security_schemes
316
- end
317
- end
318
-
319
- # Complete server configuration for Spikard.
320
- #
321
- # This is the main configuration object that controls all aspects of the server
322
- # including network settings, middleware, authentication, and more.
323
- #
324
- # @example
325
- # config = ServerConfig.new(
326
- # host: '0.0.0.0',
327
- # port: 8080,
328
- # workers: 4,
329
- # compression: CompressionConfig.new(quality: 9),
330
- # rate_limit: RateLimitConfig.new(per_second: 100, burst: 200),
331
- # static_files: [
332
- # StaticFilesConfig.new(
333
- # directory: './public',
334
- # route_prefix: '/static'
335
- # )
336
- # ],
337
- # openapi: OpenApiConfig.new(
338
- # enabled: true,
339
- # title: 'My API',
340
- # version: '1.0.0'
341
- # )
342
- # )
343
- class ServerConfig
344
- attr_accessor :host, :port, :workers,
345
- :enable_request_id, :max_body_size, :request_timeout,
346
- :compression, :rate_limit, :jwt_auth, :api_key_auth,
347
- :static_files, :graceful_shutdown, :shutdown_timeout,
348
- :openapi
349
-
350
- # @param host [String] Host address to bind to (default: "127.0.0.1")
351
- # @param port [Integer] Port number to listen on (default: 8000, range: 1-65535)
352
- # @param workers [Integer] Number of worker processes (default: 1)
353
- # @param enable_request_id [Boolean] Add X-Request-ID header to responses (default: true)
354
- # @param max_body_size [Integer, nil] Maximum request body size in bytes (default: 10MB, nil for unlimited)
355
- # @param request_timeout [Integer, nil] Request timeout in seconds (default: 30, nil for no timeout)
356
- # @param compression [CompressionConfig, nil] Response compression configuration (default: enabled with defaults)
357
- # @param rate_limit [RateLimitConfig, nil] Rate limiting configuration (default: nil/disabled)
358
- # @param jwt_auth [JwtConfig, nil] JWT authentication configuration (default: nil/disabled)
359
- # @param api_key_auth [ApiKeyConfig, nil] API key authentication configuration (default: nil/disabled)
360
- # @param static_files [Array<StaticFilesConfig>] List of static file serving configurations (default: [])
361
- # @param graceful_shutdown [Boolean] Enable graceful shutdown (default: true)
362
- # @param shutdown_timeout [Integer] Graceful shutdown timeout in seconds (default: 30)
363
- # @param openapi [OpenApiConfig, nil] OpenAPI configuration (default: nil/disabled)
364
- def initialize(
365
- host: '127.0.0.1',
366
- port: 8000,
367
- workers: 1,
368
- enable_request_id: true,
369
- max_body_size: 10 * 1024 * 1024, # 10MB
370
- request_timeout: 30,
371
- compression: CompressionConfig.new,
372
- rate_limit: nil,
373
- jwt_auth: nil,
374
- api_key_auth: nil,
375
- static_files: [],
376
- graceful_shutdown: true,
377
- shutdown_timeout: 30,
378
- openapi: nil
379
- )
380
- @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
386
- @compression = compression
387
- @rate_limit = rate_limit
388
- @jwt_auth = jwt_auth
389
- @api_key_auth = api_key_auth
390
- @static_files = static_files
391
- @graceful_shutdown = graceful_shutdown
392
- @shutdown_timeout = shutdown_timeout
393
- @openapi = openapi
394
- end
395
- end
396
- end
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 = gzip
25
+ @brotli = brotli
26
+ @min_size = min_size
27
+ @quality = quality
28
+ end
29
+ end
30
+
31
+ # Rate limiting configuration using Generic Cell Rate Algorithm (GCRA).
32
+ #
33
+ # By default, rate limits are applied per IP address.
34
+ #
35
+ # @example
36
+ # rate_limit = RateLimitConfig.new(
37
+ # per_second: 100,
38
+ # burst: 200,
39
+ # ip_based: true
40
+ # )
41
+ class RateLimitConfig
42
+ attr_accessor :per_second, :burst, :ip_based
43
+
44
+ # @param per_second [Integer] Maximum requests per second
45
+ # @param burst [Integer] Burst allowance - allows temporary spikes
46
+ # @param ip_based [Boolean] Apply rate limits per IP address (default: true)
47
+ def initialize(per_second:, burst:, ip_based: true)
48
+ @per_second = per_second
49
+ @burst = burst
50
+ @ip_based = ip_based
51
+ end
52
+ end
53
+
54
+ # JWT authentication configuration.
55
+ #
56
+ # Validates JWT tokens using the specified secret and algorithm.
57
+ # Tokens are expected in the Authorization header as "Bearer <token>".
58
+ #
59
+ # Supported algorithms:
60
+ # - HS256, HS384, HS512 (HMAC with SHA)
61
+ # - RS256, RS384, RS512 (RSA signatures)
62
+ # - ES256, ES384, ES512 (ECDSA signatures)
63
+ # - PS256, PS384, PS512 (RSA-PSS signatures)
64
+ #
65
+ # @example
66
+ # jwt = JwtConfig.new(
67
+ # secret: 'your-secret-key',
68
+ # algorithm: 'HS256',
69
+ # audience: ['api.example.com'],
70
+ # issuer: 'auth.example.com',
71
+ # leeway: 30
72
+ # )
73
+ class JwtConfig
74
+ attr_accessor :secret, :algorithm, :audience, :issuer, :leeway
75
+
76
+ # @param secret [String] Secret key for JWT validation
77
+ # @param algorithm [String] JWT algorithm (default: "HS256")
78
+ # @param audience [Array<String>, nil] Expected audience claim(s)
79
+ # @param issuer [String, nil] Expected issuer claim
80
+ # @param leeway [Integer] Time leeway in seconds for exp/nbf/iat claims (default: 0)
81
+ def initialize(secret:, algorithm: 'HS256', audience: nil, issuer: nil, leeway: 0)
82
+ @secret = secret
83
+ @algorithm = algorithm
84
+ @audience = audience
85
+ @issuer = issuer
86
+ @leeway = leeway
87
+ end
88
+ end
89
+
90
+ # API key authentication configuration.
91
+ #
92
+ # Validates API keys from request headers. Keys are matched exactly.
93
+ #
94
+ # @example
95
+ # api_key = ApiKeyConfig.new(
96
+ # keys: ['key-1', 'key-2', 'key-3'],
97
+ # header_name: 'X-API-Key'
98
+ # )
99
+ class ApiKeyConfig
100
+ attr_accessor :keys, :header_name
101
+
102
+ # @param keys [Array<String>] List of valid API keys
103
+ # @param header_name [String] HTTP header name to check for API key (default: "X-API-Key")
104
+ def initialize(keys:, header_name: 'X-API-Key')
105
+ @keys = keys
106
+ @header_name = header_name
107
+ end
108
+ end
109
+
110
+ # Static file serving configuration.
111
+ #
112
+ # Serves files from a directory at a given route prefix.
113
+ # Multiple static file configurations can be registered.
114
+ #
115
+ # @example
116
+ # static = StaticFilesConfig.new(
117
+ # directory: './public',
118
+ # route_prefix: '/static',
119
+ # index_file: true,
120
+ # cache_control: 'public, max-age=3600'
121
+ # )
122
+ class StaticFilesConfig
123
+ attr_accessor :directory, :route_prefix, :index_file, :cache_control
124
+
125
+ # @param directory [String] Directory path containing static files
126
+ # @param route_prefix [String] URL prefix for serving static files (e.g., "/static")
127
+ # @param index_file [Boolean] Serve index.html for directory requests (default: true)
128
+ # @param cache_control [String, nil] Optional Cache-Control header value (e.g., "public, max-age=3600")
129
+ def initialize(directory:, route_prefix:, index_file: true, cache_control: nil)
130
+ @directory = directory
131
+ @route_prefix = route_prefix
132
+ @index_file = index_file
133
+ @cache_control = cache_control
134
+ end
135
+ end
136
+
137
+ # Contact information for OpenAPI documentation.
138
+ #
139
+ # @example
140
+ # contact = ContactInfo.new(
141
+ # name: 'API Team',
142
+ # email: 'api@example.com',
143
+ # url: 'https://example.com'
144
+ # )
145
+ class ContactInfo
146
+ attr_accessor :name, :email, :url
147
+
148
+ # @param name [String, nil] Name of the contact person/organization
149
+ # @param email [String, nil] Email address for contact
150
+ # @param url [String, nil] URL for contact information
151
+ def initialize(name: nil, email: nil, url: nil)
152
+ @name = name
153
+ @email = email
154
+ @url = url
155
+ end
156
+ end
157
+
158
+ # License information for OpenAPI documentation.
159
+ #
160
+ # @example
161
+ # license = LicenseInfo.new(
162
+ # name: 'MIT',
163
+ # url: 'https://opensource.org/licenses/MIT'
164
+ # )
165
+ class LicenseInfo
166
+ attr_accessor :name, :url
167
+
168
+ # @param name [String] License name (e.g., "MIT", "Apache 2.0")
169
+ # @param url [String, nil] URL to the full license text
170
+ def initialize(name:, url: nil)
171
+ @name = name
172
+ @url = url
173
+ end
174
+ end
175
+
176
+ # Server information for OpenAPI documentation.
177
+ #
178
+ # Multiple servers can be specified for different environments.
179
+ #
180
+ # @example
181
+ # server = ServerInfo.new(
182
+ # url: 'https://api.example.com',
183
+ # description: 'Production'
184
+ # )
185
+ class ServerInfo
186
+ attr_accessor :url, :description
187
+
188
+ # @param url [String] Server URL (e.g., "https://api.example.com")
189
+ # @param description [String, nil] Description of the server (e.g., "Production", "Staging")
190
+ def initialize(url:, description: nil)
191
+ @url = url
192
+ @description = description
193
+ end
194
+ end
195
+
196
+ # Security scheme configuration for OpenAPI documentation.
197
+ #
198
+ # Supports HTTP (Bearer/JWT) and API Key authentication schemes.
199
+ #
200
+ # @example HTTP Bearer
201
+ # scheme = SecuritySchemeInfo.new(
202
+ # type: 'http',
203
+ # scheme: 'bearer',
204
+ # bearer_format: 'JWT'
205
+ # )
206
+ #
207
+ # @example API Key
208
+ # scheme = SecuritySchemeInfo.new(
209
+ # type: 'apiKey',
210
+ # location: 'header',
211
+ # name: 'X-API-Key'
212
+ # )
213
+ class SecuritySchemeInfo
214
+ attr_accessor :type, :scheme, :bearer_format, :location, :name
215
+
216
+ # @param type [String] Security scheme type ("http" or "apiKey")
217
+ # @param scheme [String, nil] HTTP scheme (e.g., "bearer", "basic") - for type="http"
218
+ # @param bearer_format [String, nil] Format hint for Bearer tokens (e.g., "JWT") - for type="http"
219
+ # @param location [String, nil] Where to look for the API key ("header", "query", or "cookie") - for type="apiKey"
220
+ # @param name [String, nil] Parameter name (e.g., "X-API-Key") - for type="apiKey"
221
+ def initialize(type:, scheme: nil, bearer_format: nil, location: nil, name: nil)
222
+ @type = type
223
+ @scheme = scheme
224
+ @bearer_format = bearer_format
225
+ @location = location
226
+ @name = name
227
+
228
+ validate!
229
+ end
230
+
231
+ private
232
+
233
+ def validate!
234
+ case @type
235
+ when 'http'
236
+ raise ArgumentError, 'scheme is required for type="http"' if @scheme.nil?
237
+ when 'apiKey'
238
+ raise ArgumentError, 'location and name are required for type="apiKey"' if @location.nil? || @name.nil?
239
+ else
240
+ raise ArgumentError, "type must be 'http' or 'apiKey', got: #{@type.inspect}"
241
+ end
242
+ end
243
+ end
244
+
245
+ # OpenAPI 3.1.0 documentation configuration.
246
+ #
247
+ # Spikard can automatically generate OpenAPI documentation from your routes.
248
+ # When enabled, it serves:
249
+ # - Swagger UI at /docs (customizable)
250
+ # - Redoc at /redoc (customizable)
251
+ # - OpenAPI JSON spec at /openapi.json (customizable)
252
+ #
253
+ # Security schemes are auto-detected from middleware configuration.
254
+ # Schemas are generated from your route type hints and validation.
255
+ #
256
+ # @example
257
+ # openapi = OpenApiConfig.new(
258
+ # enabled: true,
259
+ # title: 'My API',
260
+ # version: '1.0.0',
261
+ # description: 'A great API built with Spikard',
262
+ # contact: ContactInfo.new(
263
+ # name: 'API Team',
264
+ # email: 'api@example.com',
265
+ # url: 'https://example.com'
266
+ # ),
267
+ # license: LicenseInfo.new(
268
+ # name: 'MIT',
269
+ # url: 'https://opensource.org/licenses/MIT'
270
+ # ),
271
+ # servers: [
272
+ # ServerInfo.new(url: 'https://api.example.com', description: 'Production'),
273
+ # ServerInfo.new(url: 'http://localhost:8000', description: 'Development')
274
+ # ]
275
+ # )
276
+ class OpenApiConfig
277
+ attr_accessor :enabled, :title, :version, :description,
278
+ :swagger_ui_path, :redoc_path, :openapi_json_path,
279
+ :contact, :license, :servers, :security_schemes
280
+
281
+ # @param enabled [Boolean] Enable OpenAPI generation (default: false for zero overhead)
282
+ # @param title [String] API title (default: "API")
283
+ # @param version [String] API version (default: "1.0.0")
284
+ # @param description [String, nil] API description (supports Markdown)
285
+ # @param swagger_ui_path [String] Path to serve Swagger UI (default: "/docs")
286
+ # @param redoc_path [String] Path to serve Redoc (default: "/redoc")
287
+ # @param openapi_json_path [String] Path to serve OpenAPI JSON spec (default: "/openapi.json")
288
+ # @param contact [ContactInfo, nil] Contact information for the API
289
+ # @param license [LicenseInfo, nil] License information for the API
290
+ # @param servers [Array<ServerInfo>] List of server URLs for different environments (default: [])
291
+ # @param security_schemes [Hash<String, SecuritySchemeInfo>] Custom security schemes (auto-detected if not provided)
292
+ def initialize(
293
+ enabled: false,
294
+ title: 'API',
295
+ version: '1.0.0',
296
+ description: nil,
297
+ swagger_ui_path: '/docs',
298
+ redoc_path: '/redoc',
299
+ openapi_json_path: '/openapi.json',
300
+ contact: nil,
301
+ license: nil,
302
+ servers: [],
303
+ security_schemes: {}
304
+ )
305
+ @enabled = enabled
306
+ @title = title
307
+ @version = version
308
+ @description = description
309
+ @swagger_ui_path = swagger_ui_path
310
+ @redoc_path = redoc_path
311
+ @openapi_json_path = openapi_json_path
312
+ @contact = contact
313
+ @license = license
314
+ @servers = servers
315
+ @security_schemes = security_schemes
316
+ end
317
+ end
318
+
319
+ # Complete server configuration for Spikard.
320
+ #
321
+ # This is the main configuration object that controls all aspects of the server
322
+ # including network settings, middleware, authentication, and more.
323
+ #
324
+ # @example
325
+ # config = ServerConfig.new(
326
+ # host: '0.0.0.0',
327
+ # port: 8080,
328
+ # workers: 4,
329
+ # compression: CompressionConfig.new(quality: 9),
330
+ # rate_limit: RateLimitConfig.new(per_second: 100, burst: 200),
331
+ # static_files: [
332
+ # StaticFilesConfig.new(
333
+ # directory: './public',
334
+ # route_prefix: '/static'
335
+ # )
336
+ # ],
337
+ # openapi: OpenApiConfig.new(
338
+ # enabled: true,
339
+ # title: 'My API',
340
+ # version: '1.0.0'
341
+ # )
342
+ # )
343
+ class ServerConfig
344
+ attr_accessor :host, :port, :workers,
345
+ :enable_request_id, :max_body_size, :request_timeout,
346
+ :compression, :rate_limit, :jwt_auth, :api_key_auth,
347
+ :static_files, :graceful_shutdown, :shutdown_timeout,
348
+ :openapi
349
+
350
+ # @param host [String] Host address to bind to (default: "127.0.0.1")
351
+ # @param port [Integer] Port number to listen on (default: 8000, range: 1-65535)
352
+ # @param workers [Integer] Number of worker processes (default: 1)
353
+ # @param enable_request_id [Boolean] Add X-Request-ID header to responses (default: true)
354
+ # @param max_body_size [Integer, nil] Maximum request body size in bytes (default: 10MB, nil for unlimited)
355
+ # @param request_timeout [Integer, nil] Request timeout in seconds (default: 30, nil for no timeout)
356
+ # @param compression [CompressionConfig, nil] Response compression configuration (default: enabled with defaults)
357
+ # @param rate_limit [RateLimitConfig, nil] Rate limiting configuration (default: nil/disabled)
358
+ # @param jwt_auth [JwtConfig, nil] JWT authentication configuration (default: nil/disabled)
359
+ # @param api_key_auth [ApiKeyConfig, nil] API key authentication configuration (default: nil/disabled)
360
+ # @param static_files [Array<StaticFilesConfig>] List of static file serving configurations (default: [])
361
+ # @param graceful_shutdown [Boolean] Enable graceful shutdown (default: true)
362
+ # @param shutdown_timeout [Integer] Graceful shutdown timeout in seconds (default: 30)
363
+ # @param openapi [OpenApiConfig, nil] OpenAPI configuration (default: nil/disabled)
364
+ def initialize(
365
+ host: '127.0.0.1',
366
+ port: 8000,
367
+ workers: 1,
368
+ enable_request_id: true,
369
+ max_body_size: 10 * 1024 * 1024, # 10MB
370
+ request_timeout: 30,
371
+ compression: CompressionConfig.new,
372
+ rate_limit: nil,
373
+ jwt_auth: nil,
374
+ api_key_auth: nil,
375
+ static_files: [],
376
+ graceful_shutdown: true,
377
+ shutdown_timeout: 30,
378
+ openapi: nil
379
+ )
380
+ @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
386
+ @compression = compression
387
+ @rate_limit = rate_limit
388
+ @jwt_auth = jwt_auth
389
+ @api_key_auth = api_key_auth
390
+ @static_files = static_files
391
+ @graceful_shutdown = graceful_shutdown
392
+ @shutdown_timeout = shutdown_timeout
393
+ @openapi = openapi
394
+ end
395
+ end
396
+ end