spikard 0.4.0-x64-mingw-ucrt

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 (138) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +1 -0
  3. data/README.md +659 -0
  4. data/ext/spikard_rb/Cargo.toml +17 -0
  5. data/ext/spikard_rb/extconf.rb +10 -0
  6. data/ext/spikard_rb/src/lib.rs +6 -0
  7. data/lib/spikard/app.rb +405 -0
  8. data/lib/spikard/background.rb +27 -0
  9. data/lib/spikard/config.rb +396 -0
  10. data/lib/spikard/converters.rb +13 -0
  11. data/lib/spikard/handler_wrapper.rb +113 -0
  12. data/lib/spikard/provide.rb +214 -0
  13. data/lib/spikard/response.rb +173 -0
  14. data/lib/spikard/schema.rb +243 -0
  15. data/lib/spikard/sse.rb +111 -0
  16. data/lib/spikard/streaming_response.rb +44 -0
  17. data/lib/spikard/testing.rb +221 -0
  18. data/lib/spikard/upload_file.rb +131 -0
  19. data/lib/spikard/version.rb +5 -0
  20. data/lib/spikard/websocket.rb +59 -0
  21. data/lib/spikard.rb +43 -0
  22. data/sig/spikard.rbs +366 -0
  23. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +5 -0
  24. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +2 -0
  25. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  26. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +139 -0
  27. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +561 -0
  28. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  29. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  30. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +403 -0
  31. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +274 -0
  32. data/vendor/crates/spikard-bindings-shared/src/lib.rs +25 -0
  33. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +298 -0
  34. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +637 -0
  35. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +309 -0
  36. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  37. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +355 -0
  38. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +502 -0
  39. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +389 -0
  40. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +413 -0
  41. data/vendor/crates/spikard-core/Cargo.toml +40 -0
  42. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -0
  43. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -0
  44. data/vendor/crates/spikard-core/src/debug.rs +63 -0
  45. data/vendor/crates/spikard-core/src/di/container.rs +726 -0
  46. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -0
  47. data/vendor/crates/spikard-core/src/di/error.rs +118 -0
  48. data/vendor/crates/spikard-core/src/di/factory.rs +538 -0
  49. data/vendor/crates/spikard-core/src/di/graph.rs +545 -0
  50. data/vendor/crates/spikard-core/src/di/mod.rs +192 -0
  51. data/vendor/crates/spikard-core/src/di/resolved.rs +411 -0
  52. data/vendor/crates/spikard-core/src/di/value.rs +283 -0
  53. data/vendor/crates/spikard-core/src/errors.rs +39 -0
  54. data/vendor/crates/spikard-core/src/http.rs +153 -0
  55. data/vendor/crates/spikard-core/src/lib.rs +29 -0
  56. data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
  57. data/vendor/crates/spikard-core/src/metadata.rs +397 -0
  58. data/vendor/crates/spikard-core/src/parameters.rs +723 -0
  59. data/vendor/crates/spikard-core/src/problem.rs +310 -0
  60. data/vendor/crates/spikard-core/src/request_data.rs +189 -0
  61. data/vendor/crates/spikard-core/src/router.rs +249 -0
  62. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -0
  63. data/vendor/crates/spikard-core/src/type_hints.rs +304 -0
  64. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +689 -0
  65. data/vendor/crates/spikard-core/src/validation/mod.rs +459 -0
  66. data/vendor/crates/spikard-http/Cargo.toml +58 -0
  67. data/vendor/crates/spikard-http/examples/sse-notifications.rs +147 -0
  68. data/vendor/crates/spikard-http/examples/websocket-chat.rs +91 -0
  69. data/vendor/crates/spikard-http/src/auth.rs +247 -0
  70. data/vendor/crates/spikard-http/src/background.rs +1562 -0
  71. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -0
  72. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -0
  73. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -0
  74. data/vendor/crates/spikard-http/src/cors.rs +490 -0
  75. data/vendor/crates/spikard-http/src/debug.rs +63 -0
  76. data/vendor/crates/spikard-http/src/di_handler.rs +1878 -0
  77. data/vendor/crates/spikard-http/src/handler_response.rs +532 -0
  78. data/vendor/crates/spikard-http/src/handler_trait.rs +861 -0
  79. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
  80. data/vendor/crates/spikard-http/src/lib.rs +524 -0
  81. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -0
  82. data/vendor/crates/spikard-http/src/lifecycle.rs +428 -0
  83. data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -0
  84. data/vendor/crates/spikard-http/src/middleware/multipart.rs +930 -0
  85. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +541 -0
  86. data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -0
  87. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -0
  88. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -0
  89. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +867 -0
  90. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +678 -0
  91. data/vendor/crates/spikard-http/src/query_parser.rs +369 -0
  92. data/vendor/crates/spikard-http/src/response.rs +399 -0
  93. data/vendor/crates/spikard-http/src/server/handler.rs +1557 -0
  94. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
  95. data/vendor/crates/spikard-http/src/server/mod.rs +806 -0
  96. data/vendor/crates/spikard-http/src/server/request_extraction.rs +630 -0
  97. data/vendor/crates/spikard-http/src/server/routing_factory.rs +497 -0
  98. data/vendor/crates/spikard-http/src/sse.rs +961 -0
  99. data/vendor/crates/spikard-http/src/testing/form.rs +14 -0
  100. data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -0
  101. data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -0
  102. data/vendor/crates/spikard-http/src/testing.rs +377 -0
  103. data/vendor/crates/spikard-http/src/websocket.rs +831 -0
  104. data/vendor/crates/spikard-http/tests/background_behavior.rs +918 -0
  105. data/vendor/crates/spikard-http/tests/common/handlers.rs +308 -0
  106. data/vendor/crates/spikard-http/tests/common/mod.rs +21 -0
  107. data/vendor/crates/spikard-http/tests/di_integration.rs +202 -0
  108. data/vendor/crates/spikard-http/tests/doc_snippets.rs +4 -0
  109. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1135 -0
  110. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +688 -0
  111. data/vendor/crates/spikard-http/tests/server_config_builder.rs +324 -0
  112. data/vendor/crates/spikard-http/tests/sse_behavior.rs +728 -0
  113. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +724 -0
  114. data/vendor/crates/spikard-rb/Cargo.toml +43 -0
  115. data/vendor/crates/spikard-rb/build.rs +199 -0
  116. data/vendor/crates/spikard-rb/src/background.rs +63 -0
  117. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  118. data/vendor/crates/spikard-rb/src/config/server_config.rs +283 -0
  119. data/vendor/crates/spikard-rb/src/conversion.rs +459 -0
  120. data/vendor/crates/spikard-rb/src/di/builder.rs +105 -0
  121. data/vendor/crates/spikard-rb/src/di/mod.rs +413 -0
  122. data/vendor/crates/spikard-rb/src/handler.rs +612 -0
  123. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  124. data/vendor/crates/spikard-rb/src/lib.rs +1857 -0
  125. data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -0
  126. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  127. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +427 -0
  128. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  129. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +326 -0
  130. data/vendor/crates/spikard-rb/src/server.rs +283 -0
  131. data/vendor/crates/spikard-rb/src/sse.rs +231 -0
  132. data/vendor/crates/spikard-rb/src/testing/client.rs +404 -0
  133. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  134. data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -0
  135. data/vendor/crates/spikard-rb/src/testing/websocket.rs +221 -0
  136. data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
  137. data/vendor/crates/spikard-rb/tests/magnus_ffi_tests.rs +14 -0
  138. metadata +213 -0
@@ -0,0 +1,43 @@
1
+ [package]
2
+ name = "spikard-rb"
3
+ version = "0.4.0"
4
+ edition = "2024"
5
+ authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
6
+ license = "MIT"
7
+ repository = "https://github.com/Goldziher/spikard"
8
+ homepage = "https://github.com/Goldziher/spikard"
9
+ description = "High-performance Ruby bindings for Spikard HTTP framework via Magnus"
10
+ keywords = ["ruby", "bindings", "magnus", "http", "framework"]
11
+ categories = ["api-bindings", "web-programming::http-server"]
12
+ documentation = "https://docs.rs/spikard-rb"
13
+ readme = "README.md"
14
+ build = "build.rs"
15
+
16
+ [lib]
17
+ name = "spikard_rb_core"
18
+
19
+ [dependencies]
20
+ magnus = { version = "0.8.2", features = ["rb-sys"] }
21
+ serde = { version = "1.0", features = ["derive"] }
22
+ serde_json = "1.0"
23
+ spikard-core = { path = "../spikard-core", features = ["di"] }
24
+ spikard-http = { path = "../spikard-http", features = ["di"] }
25
+ spikard-bindings-shared = { path = "../spikard-bindings-shared" }
26
+ axum = { version = "0.8", features = ["multipart", "ws"] }
27
+ axum-test = "18"
28
+ bytes = "1.11"
29
+ cookie = "0.18"
30
+ tokio = { version = "1", features = ["full"] }
31
+ tracing = "0.1"
32
+ serde_qs = "0.15"
33
+ urlencoding = "2.1"
34
+ once_cell = "1.21"
35
+ async-stream = "0.3"
36
+ http = "1.4"
37
+
38
+ [features]
39
+ default = ["di"]
40
+ di = ["spikard-core/di", "spikard-http/di"]
41
+
42
+ [build-dependencies]
43
+ rb-sys-build = "0.9"
@@ -0,0 +1,199 @@
1
+ use std::env;
2
+ use std::fs;
3
+ use std::path::PathBuf;
4
+
5
+ fn main() -> Result<(), Box<dyn std::error::Error>> {
6
+ println!("cargo:rerun-if-changed=build.rs");
7
+
8
+ let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
9
+ let workspace_root = manifest_dir
10
+ .parent()
11
+ .and_then(|p| p.parent())
12
+ .ok_or("Failed to resolve workspace root from manifest dir")?;
13
+
14
+ let target = workspace_root.join("packages/ruby/lib/spikard/response.rb");
15
+ if let Some(parent) = target.parent() {
16
+ fs::create_dir_all(parent)?;
17
+ }
18
+
19
+ fs::write(&target, generated_response_file())?;
20
+ Ok(())
21
+ }
22
+
23
+ fn generated_response_file() -> String {
24
+ r##"# frozen_string_literal: true
25
+
26
+ # ⚠️ GENERATED BY crates/spikard-rb/build.rs — DO NOT EDIT BY HAND
27
+ module Spikard
28
+ class Response # :nodoc: Native-backed HTTP response facade generated from Rust metadata.
29
+ attr_reader :content, :status_code, :headers, :native_response
30
+
31
+ def initialize(content: nil, body: nil, status_code: 200, headers: nil, content_type: nil)
32
+ @content = content.nil? ? body : content
33
+ @status_code = Integer(status_code || 200)
34
+ @headers = normalize_headers(headers)
35
+ set_header('content-type', content_type) if content_type
36
+ rebuild_native!
37
+ end
38
+
39
+ def status
40
+ @status_code
41
+ end
42
+
43
+ def status_code=(value)
44
+ @status_code = Integer(value)
45
+ rebuild_native!
46
+ rescue ArgumentError, TypeError
47
+ raise ArgumentError, 'status_code must be an integer'
48
+ end
49
+
50
+ def headers=(value)
51
+ @headers = normalize_headers(value)
52
+ rebuild_native!
53
+ end
54
+
55
+ def content=(value)
56
+ @content = value
57
+ rebuild_native!
58
+ end
59
+
60
+ def set_header(name, value)
61
+ @headers[name.to_s] = value.to_s
62
+ rebuild_native!
63
+ end
64
+
65
+ def set_cookie(name, value, **options)
66
+ raise ArgumentError, 'cookie name required' if name.nil? || name.empty?
67
+
68
+ header_value = ["#{name}=#{value}", *cookie_parts(options)].join('; ')
69
+ set_header('set-cookie', header_value)
70
+ end
71
+
72
+ def to_native_response
73
+ @native_response
74
+ end
75
+
76
+ private
77
+
78
+ def rebuild_native!
79
+ ensure_native!
80
+ @native_response = Spikard::Native.build_response(@content, @status_code, @headers)
81
+ return unless @native_response
82
+
83
+ @status_code = @native_response.status_code
84
+ @headers = @native_response.headers
85
+ end
86
+
87
+ def ensure_native!
88
+ return if defined?(Spikard::Native) && Spikard::Native.respond_to?(:build_response)
89
+
90
+ raise 'Spikard native extension is not loaded'
91
+ end
92
+
93
+ def cookie_parts(options)
94
+ [
95
+ options[:max_age] && "Max-Age=#{Integer(options[:max_age])}",
96
+ options[:domain] && "Domain=#{options[:domain]}",
97
+ "Path=#{options.fetch(:path, '/') || '/'}",
98
+ options[:secure] ? 'Secure' : nil,
99
+ options[:httponly] ? 'HttpOnly' : nil,
100
+ options[:samesite] && "SameSite=#{options[:samesite]}"
101
+ ].compact
102
+ end
103
+
104
+ def normalize_headers(value)
105
+ case value
106
+ when nil
107
+ {}
108
+ when Hash
109
+ value.each_with_object({}) do |(key, val), acc|
110
+ acc[key.to_s.downcase] = val.to_s
111
+ end
112
+ else
113
+ raise ArgumentError, 'headers must be a Hash'
114
+ end
115
+ end
116
+ end
117
+
118
+ class StreamingResponse # :nodoc: Streaming response wrapper backed by the native Rust builder.
119
+ attr_reader :stream, :status_code, :headers, :native_response
120
+
121
+ def initialize(stream, status_code: 200, headers: nil)
122
+ unless stream.respond_to?(:next) || stream.respond_to?(:each)
123
+ raise ArgumentError, 'StreamingResponse requires an object responding to #next or #each'
124
+ end
125
+
126
+ @stream = stream.respond_to?(:to_enum) ? stream.to_enum : stream
127
+ @status_code = Integer(status_code || 200)
128
+ header_hash = headers || {}
129
+ @headers = header_hash.each_with_object({}) do |(key, value), memo|
130
+ memo[String(key)] = String(value)
131
+ end
132
+
133
+ rebuild_native!
134
+ end
135
+
136
+ def to_native_response
137
+ @native_response
138
+ end
139
+
140
+ private
141
+
142
+ def rebuild_native!
143
+ ensure_native!
144
+ @native_response = Spikard::Native.build_streaming_response(@stream, @status_code, @headers)
145
+ return unless @native_response
146
+
147
+ @status_code = @native_response.status_code
148
+ @headers = @native_response.headers
149
+ end
150
+
151
+ def ensure_native!
152
+ return if defined?(Spikard::Native) && Spikard::Native.respond_to?(:build_streaming_response)
153
+
154
+ raise 'Spikard native extension is not loaded'
155
+ end
156
+ end
157
+
158
+ module Testing
159
+ class Response # :nodoc: Lightweight response wrapper used by the test client.
160
+ attr_reader :status_code, :headers, :body
161
+
162
+ def initialize(payload)
163
+ @status_code = payload[:status_code]
164
+ @headers = payload[:headers] || {}
165
+ @body = payload[:body]
166
+ @body_text = payload[:body_text]
167
+ end
168
+
169
+ def status
170
+ @status_code
171
+ end
172
+
173
+ def body_bytes
174
+ @body || ''.b
175
+ end
176
+
177
+ def body_text
178
+ @body_text || @body&.dup&.force_encoding(Encoding::UTF_8)
179
+ end
180
+
181
+ def text
182
+ body_text
183
+ end
184
+
185
+ def json
186
+ return nil if @body.nil? || @body.empty?
187
+
188
+ JSON.parse(@body)
189
+ end
190
+
191
+ def bytes
192
+ body_bytes.bytes
193
+ end
194
+ end
195
+ end
196
+ end
197
+ "##
198
+ .to_string()
199
+ }
@@ -0,0 +1,63 @@
1
+ use magnus::prelude::*;
2
+ use magnus::value::{InnerValue, Opaque};
3
+ use magnus::{Error, Ruby, Value};
4
+ use once_cell::sync::Lazy;
5
+ use spikard_http::{BackgroundHandle, BackgroundJobError, BackgroundJobMetadata};
6
+ use std::sync::{Arc, RwLock};
7
+
8
+ static BACKGROUND_HANDLE: Lazy<RwLock<Option<BackgroundHandle>>> = Lazy::new(|| RwLock::new(None));
9
+
10
+ pub fn install_handle(handle: BackgroundHandle) {
11
+ match BACKGROUND_HANDLE.write() {
12
+ Ok(mut guard) => *guard = Some(handle),
13
+ Err(_) => eprintln!("warning: background handle lock poisoned, continuing"),
14
+ }
15
+ }
16
+
17
+ pub fn clear_handle() {
18
+ match BACKGROUND_HANDLE.write() {
19
+ Ok(mut guard) => *guard = None,
20
+ Err(_) => eprintln!("warning: background handle lock poisoned, continuing"),
21
+ }
22
+ }
23
+
24
+ pub fn background_run(ruby: &Ruby, block: Value) -> Result<(), Error> {
25
+ let call_sym = ruby.intern("call");
26
+ if !block.respond_to(call_sym, false)? {
27
+ return Err(Error::new(
28
+ ruby.exception_type_error(),
29
+ "background.run expects a callable block",
30
+ ));
31
+ }
32
+
33
+ let handle = BACKGROUND_HANDLE
34
+ .read()
35
+ .map_err(|_| Error::new(ruby.exception_runtime_error(), "background handle lock poisoned"))?
36
+ .clone()
37
+ .ok_or_else(|| Error::new(ruby.exception_runtime_error(), "background runtime not initialized"))?;
38
+
39
+ let proc_value = Arc::new(Opaque::from(block));
40
+
41
+ handle
42
+ .spawn_with_metadata(
43
+ async move {
44
+ let proc_clone = proc_value.clone();
45
+ tokio::task::spawn_blocking(move || -> Result<(), BackgroundJobError> {
46
+ let ruby = Ruby::get().map_err(|e| BackgroundJobError::from(e.to_string()))?;
47
+ let callable = proc_clone.get_inner_with(&ruby);
48
+ callable
49
+ .funcall::<_, _, Value>("call", ())
50
+ .map(|_| ())
51
+ .map_err(|err| BackgroundJobError::from(format_ruby_error(err)))
52
+ })
53
+ .await
54
+ .map_err(|e| BackgroundJobError::from(e.to_string()))?
55
+ },
56
+ BackgroundJobMetadata::default(),
57
+ )
58
+ .map_err(|err| Error::new(ruby.exception_runtime_error(), err.to_string()))
59
+ }
60
+
61
+ fn format_ruby_error(err: Error) -> String {
62
+ err.to_string()
63
+ }
@@ -0,0 +1,5 @@
1
+ //! Server configuration extraction from Ruby objects.
2
+
3
+ pub mod server_config;
4
+
5
+ pub use server_config::extract_server_config;
@@ -0,0 +1,283 @@
1
+ //! ServerConfig extraction from Ruby objects.
2
+ //!
3
+ //! This module handles converting Ruby ServerConfig objects to the Rust
4
+ //! spikard_http::ServerConfig type.
5
+
6
+ use magnus::prelude::*;
7
+ use magnus::{Error, RArray, RHash, Ruby, TryConvert, Value};
8
+ use spikard_http::{
9
+ ApiKeyConfig, CompressionConfig, ContactInfo, JwtConfig, LicenseInfo, OpenApiConfig, RateLimitConfig, ServerInfo,
10
+ StaticFilesConfig,
11
+ };
12
+ use std::collections::HashMap;
13
+
14
+ /// Extract ServerConfig from Ruby ServerConfig object
15
+ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<spikard_http::ServerConfig, Error> {
16
+ let host: String = config_value.funcall("host", ())?;
17
+
18
+ let port: u32 = config_value.funcall("port", ())?;
19
+
20
+ let workers: usize = config_value.funcall("workers", ())?;
21
+
22
+ let enable_request_id: bool = config_value.funcall("enable_request_id", ())?;
23
+
24
+ let max_body_size_value: Value = config_value.funcall("max_body_size", ())?;
25
+ let max_body_size = if max_body_size_value.is_nil() {
26
+ None
27
+ } else {
28
+ Some(u64::try_convert(max_body_size_value)? as usize)
29
+ };
30
+
31
+ let request_timeout_value: Value = config_value.funcall("request_timeout", ())?;
32
+ let request_timeout = if request_timeout_value.is_nil() {
33
+ None
34
+ } else {
35
+ Some(u64::try_convert(request_timeout_value)?)
36
+ };
37
+
38
+ let graceful_shutdown: bool = config_value.funcall("graceful_shutdown", ())?;
39
+
40
+ let shutdown_timeout: u64 = config_value.funcall("shutdown_timeout", ())?;
41
+
42
+ let compression_value: Value = config_value.funcall("compression", ())?;
43
+ let compression = if compression_value.is_nil() {
44
+ None
45
+ } else {
46
+ let gzip: bool = compression_value.funcall("gzip", ())?;
47
+ let brotli: bool = compression_value.funcall("brotli", ())?;
48
+ let min_size: usize = compression_value.funcall("min_size", ())?;
49
+ let quality: u32 = compression_value.funcall("quality", ())?;
50
+ Some(CompressionConfig {
51
+ gzip,
52
+ brotli,
53
+ min_size,
54
+ quality,
55
+ })
56
+ };
57
+
58
+ let rate_limit_value: Value = config_value.funcall("rate_limit", ())?;
59
+ let rate_limit = if rate_limit_value.is_nil() {
60
+ None
61
+ } else {
62
+ let per_second: u64 = rate_limit_value.funcall("per_second", ())?;
63
+ let burst: u32 = rate_limit_value.funcall("burst", ())?;
64
+ let ip_based: bool = rate_limit_value.funcall("ip_based", ())?;
65
+ Some(RateLimitConfig {
66
+ per_second,
67
+ burst,
68
+ ip_based,
69
+ })
70
+ };
71
+
72
+ let jwt_auth_value: Value = config_value.funcall("jwt_auth", ())?;
73
+ let jwt_auth = if jwt_auth_value.is_nil() {
74
+ None
75
+ } else {
76
+ let secret: String = jwt_auth_value.funcall("secret", ())?;
77
+ let algorithm: String = jwt_auth_value.funcall("algorithm", ())?;
78
+ let audience_value: Value = jwt_auth_value.funcall("audience", ())?;
79
+ let audience = if audience_value.is_nil() {
80
+ None
81
+ } else {
82
+ Some(Vec::<String>::try_convert(audience_value)?)
83
+ };
84
+ let issuer_value: Value = jwt_auth_value.funcall("issuer", ())?;
85
+ let issuer = if issuer_value.is_nil() {
86
+ None
87
+ } else {
88
+ Some(String::try_convert(issuer_value)?)
89
+ };
90
+ let leeway: u64 = jwt_auth_value.funcall("leeway", ())?;
91
+ Some(JwtConfig {
92
+ secret,
93
+ algorithm,
94
+ audience,
95
+ issuer,
96
+ leeway,
97
+ })
98
+ };
99
+
100
+ let api_key_auth_value: Value = config_value.funcall("api_key_auth", ())?;
101
+ let api_key_auth = if api_key_auth_value.is_nil() {
102
+ None
103
+ } else {
104
+ let keys: Vec<String> = api_key_auth_value.funcall("keys", ())?;
105
+ let header_name: String = api_key_auth_value.funcall("header_name", ())?;
106
+ Some(ApiKeyConfig { keys, header_name })
107
+ };
108
+
109
+ let static_files_value: Value = config_value.funcall("static_files", ())?;
110
+ let static_files_array = RArray::from_value(static_files_value)
111
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "static_files must be an Array"))?;
112
+
113
+ let mut static_files = Vec::new();
114
+ for i in 0..static_files_array.len() {
115
+ let sf_value = static_files_array.entry::<Value>(i as isize)?;
116
+ let directory: String = sf_value.funcall("directory", ())?;
117
+ let route_prefix: String = sf_value.funcall("route_prefix", ())?;
118
+ let index_file: bool = sf_value.funcall("index_file", ())?;
119
+ let cache_control_value: Value = sf_value.funcall("cache_control", ())?;
120
+ let cache_control = if cache_control_value.is_nil() {
121
+ None
122
+ } else {
123
+ Some(String::try_convert(cache_control_value)?)
124
+ };
125
+ static_files.push(StaticFilesConfig {
126
+ directory,
127
+ route_prefix,
128
+ index_file,
129
+ cache_control,
130
+ });
131
+ }
132
+
133
+ let openapi_value: Value = config_value.funcall("openapi", ())?;
134
+ let openapi = if openapi_value.is_nil() {
135
+ None
136
+ } else {
137
+ let enabled: bool = openapi_value.funcall("enabled", ())?;
138
+ let title: String = openapi_value.funcall("title", ())?;
139
+ let version: String = openapi_value.funcall("version", ())?;
140
+ let description_value: Value = openapi_value.funcall("description", ())?;
141
+ let description = if description_value.is_nil() {
142
+ None
143
+ } else {
144
+ Some(String::try_convert(description_value)?)
145
+ };
146
+ let swagger_ui_path: String = openapi_value.funcall("swagger_ui_path", ())?;
147
+ let redoc_path: String = openapi_value.funcall("redoc_path", ())?;
148
+ let openapi_json_path: String = openapi_value.funcall("openapi_json_path", ())?;
149
+
150
+ let contact_value: Value = openapi_value.funcall("contact", ())?;
151
+ let contact = if contact_value.is_nil() {
152
+ None
153
+ } else if let Some(contact_hash) = RHash::from_value(contact_value) {
154
+ let name = get_optional_string_from_hash(contact_hash, "name")?;
155
+ let email = get_optional_string_from_hash(contact_hash, "email")?;
156
+ let url = get_optional_string_from_hash(contact_hash, "url")?;
157
+ Some(ContactInfo { name, email, url })
158
+ } else {
159
+ let name_value: Value = contact_value.funcall("name", ())?;
160
+ let email_value: Value = contact_value.funcall("email", ())?;
161
+ let url_value: Value = contact_value.funcall("url", ())?;
162
+ Some(ContactInfo {
163
+ name: if name_value.is_nil() {
164
+ None
165
+ } else {
166
+ Some(String::try_convert(name_value)?)
167
+ },
168
+ email: if email_value.is_nil() {
169
+ None
170
+ } else {
171
+ Some(String::try_convert(email_value)?)
172
+ },
173
+ url: if url_value.is_nil() {
174
+ None
175
+ } else {
176
+ Some(String::try_convert(url_value)?)
177
+ },
178
+ })
179
+ };
180
+
181
+ let license_value: Value = openapi_value.funcall("license", ())?;
182
+ let license = if license_value.is_nil() {
183
+ None
184
+ } else if let Some(license_hash) = RHash::from_value(license_value) {
185
+ let name = get_required_string_from_hash(license_hash, "name", ruby)?;
186
+ let url = get_optional_string_from_hash(license_hash, "url")?;
187
+ Some(LicenseInfo { name, url })
188
+ } else {
189
+ let name: String = license_value.funcall("name", ())?;
190
+ let url_value: Value = license_value.funcall("url", ())?;
191
+ let url = if url_value.is_nil() {
192
+ None
193
+ } else {
194
+ Some(String::try_convert(url_value)?)
195
+ };
196
+ Some(LicenseInfo { name, url })
197
+ };
198
+
199
+ let servers_value: Value = openapi_value.funcall("servers", ())?;
200
+ let servers_array = RArray::from_value(servers_value)
201
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "servers must be an Array"))?;
202
+
203
+ let mut servers = Vec::new();
204
+ for i in 0..servers_array.len() {
205
+ let server_value = servers_array.entry::<Value>(i as isize)?;
206
+
207
+ let (url, description) = if let Some(server_hash) = RHash::from_value(server_value) {
208
+ let url = get_required_string_from_hash(server_hash, "url", ruby)?;
209
+ let description = get_optional_string_from_hash(server_hash, "description")?;
210
+ (url, description)
211
+ } else {
212
+ let url: String = server_value.funcall("url", ())?;
213
+ let description_value: Value = server_value.funcall("description", ())?;
214
+ let description = if description_value.is_nil() {
215
+ None
216
+ } else {
217
+ Some(String::try_convert(description_value)?)
218
+ };
219
+ (url, description)
220
+ };
221
+
222
+ servers.push(ServerInfo { url, description });
223
+ }
224
+
225
+ let security_schemes = HashMap::new();
226
+
227
+ Some(OpenApiConfig {
228
+ enabled,
229
+ title,
230
+ version,
231
+ description,
232
+ swagger_ui_path,
233
+ redoc_path,
234
+ openapi_json_path,
235
+ contact,
236
+ license,
237
+ servers,
238
+ security_schemes,
239
+ })
240
+ };
241
+
242
+ Ok(spikard_http::ServerConfig {
243
+ host,
244
+ port: port as u16,
245
+ workers,
246
+ enable_request_id,
247
+ max_body_size,
248
+ request_timeout,
249
+ compression,
250
+ rate_limit,
251
+ jwt_auth,
252
+ api_key_auth,
253
+ static_files,
254
+ graceful_shutdown,
255
+ shutdown_timeout,
256
+ background_tasks: spikard_http::BackgroundTaskConfig::default(),
257
+ openapi,
258
+ lifecycle_hooks: None,
259
+ di_container: None,
260
+ })
261
+ }
262
+
263
+ /// Helper to extract an optional string from a Ruby Hash
264
+ pub fn get_optional_string_from_hash(hash: RHash, key: &str) -> Result<Option<String>, Error> {
265
+ match hash.get(String::from(key)) {
266
+ Some(v) if !v.is_nil() => Ok(Some(String::try_convert(v)?)),
267
+ _ => Ok(None),
268
+ }
269
+ }
270
+
271
+ /// Helper to extract a required string from a Ruby Hash
272
+ pub fn get_required_string_from_hash(hash: RHash, key: &str, ruby: &Ruby) -> Result<String, Error> {
273
+ let value = hash
274
+ .get(String::from(key))
275
+ .ok_or_else(|| Error::new(ruby.exception_arg_error(), format!("missing required key '{}'", key)))?;
276
+ if value.is_nil() {
277
+ return Err(Error::new(
278
+ ruby.exception_arg_error(),
279
+ format!("key '{}' cannot be nil", key),
280
+ ));
281
+ }
282
+ String::try_convert(value)
283
+ }