spikard 0.3.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -6
  3. data/ext/spikard_rb/Cargo.toml +2 -2
  4. data/lib/spikard/app.rb +33 -14
  5. data/lib/spikard/testing.rb +47 -12
  6. data/lib/spikard/version.rb +1 -1
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  8. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  9. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  10. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  11. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  12. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  13. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  14. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  15. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  16. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  17. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  18. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  19. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  20. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  21. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  22. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  23. data/vendor/crates/spikard-core/Cargo.toml +4 -4
  24. data/vendor/crates/spikard-core/src/debug.rs +64 -0
  25. data/vendor/crates/spikard-core/src/di/container.rs +3 -27
  26. data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
  27. data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
  28. data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
  29. data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
  30. data/vendor/crates/spikard-core/src/di/value.rs +2 -4
  31. data/vendor/crates/spikard-core/src/errors.rs +30 -0
  32. data/vendor/crates/spikard-core/src/http.rs +262 -0
  33. data/vendor/crates/spikard-core/src/lib.rs +1 -1
  34. data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
  35. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  36. data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
  37. data/vendor/crates/spikard-core/src/problem.rs +34 -0
  38. data/vendor/crates/spikard-core/src/request_data.rs +966 -1
  39. data/vendor/crates/spikard-core/src/router.rs +263 -2
  40. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  41. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
  42. data/vendor/crates/spikard-http/Cargo.toml +12 -16
  43. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  44. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  45. data/vendor/crates/spikard-http/src/auth.rs +65 -16
  46. data/vendor/crates/spikard-http/src/background.rs +1614 -3
  47. data/vendor/crates/spikard-http/src/cors.rs +515 -0
  48. data/vendor/crates/spikard-http/src/debug.rs +65 -0
  49. data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
  50. data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
  51. data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
  52. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
  53. data/vendor/crates/spikard-http/src/lib.rs +33 -28
  54. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
  55. data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
  56. data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
  57. data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
  58. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
  59. data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
  60. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
  61. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
  62. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
  63. data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
  64. data/vendor/crates/spikard-http/src/response.rs +321 -0
  65. data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
  66. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
  67. data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
  68. data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
  69. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  70. data/vendor/crates/spikard-http/src/sse.rs +983 -21
  71. data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
  72. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
  73. data/vendor/crates/spikard-http/src/testing.rs +7 -7
  74. data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
  75. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  76. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  77. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  78. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  79. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  80. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  81. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  82. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  83. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  84. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  85. data/vendor/crates/spikard-rb/Cargo.toml +10 -4
  86. data/vendor/crates/spikard-rb/build.rs +196 -5
  87. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  88. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
  89. data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
  90. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  91. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
  92. data/vendor/crates/spikard-rb/src/handler.rs +100 -107
  93. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  94. data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  97. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  98. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  99. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  100. data/vendor/crates/spikard-rb/src/server.rs +47 -22
  101. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
  102. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  103. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  104. data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
  105. metadata +46 -13
  106. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  107. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  108. data/vendor/crates/spikard-http/src/router.rs +0 -1
  109. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  110. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  111. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  112. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  113. /data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +0 -0
@@ -1,8 +1,199 @@
1
- use rb_sys_build::RbConfig;
1
+ use std::env;
2
+ use std::fs;
3
+ use std::path::PathBuf;
2
4
 
3
- fn main() {
4
- let mut rbconfig = RbConfig::current();
5
- rbconfig.link_ruby(false);
6
- rbconfig.print_cargo_args();
5
+ fn main() -> Result<(), Box<dyn std::error::Error>> {
7
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()
8
199
  }
@@ -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;
@@ -1,25 +1,18 @@
1
- //! ServerConfig extraction from Ruby configuration objects.
1
+ //! ServerConfig extraction from Ruby objects.
2
2
  //!
3
- //! This module handles conversion of Ruby ServerConfig objects
4
- //! into Rust ServerConfig structs for Spikard HTTP server setup.
5
-
6
- #![allow(dead_code)]
3
+ //! This module handles converting Ruby ServerConfig objects to the Rust
4
+ //! spikard_http::ServerConfig type.
7
5
 
8
6
  use magnus::prelude::*;
9
7
  use magnus::{Error, RArray, RHash, Ruby, TryConvert, Value};
10
8
  use spikard_http::{
11
- ApiKeyConfig, CompressionConfig, ContactInfo, JwtConfig, LicenseInfo, OpenApiConfig, RateLimitConfig, ServerConfig,
12
- ServerInfo, StaticFilesConfig,
9
+ ApiKeyConfig, CompressionConfig, ContactInfo, JwtConfig, LicenseInfo, OpenApiConfig, RateLimitConfig, ServerInfo,
10
+ StaticFilesConfig,
13
11
  };
14
12
  use std::collections::HashMap;
15
13
 
16
- use crate::conversion::{get_optional_string_from_hash, get_required_string_from_hash};
17
-
18
- /// Extract a ServerConfig from a Ruby configuration object.
19
- ///
20
- /// Handles all ServerConfig properties including middleware configs,
21
- /// authentication, static files, and OpenAPI documentation settings.
22
- pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<ServerConfig, Error> {
14
+ /// Extract ServerConfig from Ruby ServerConfig object
15
+ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<spikard_http::ServerConfig, Error> {
23
16
  let host: String = config_value.funcall("host", ())?;
24
17
 
25
18
  let port: u32 = config_value.funcall("port", ())?;
@@ -46,7 +39,6 @@ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<ServerC
46
39
 
47
40
  let shutdown_timeout: u64 = config_value.funcall("shutdown_timeout", ())?;
48
41
 
49
- // Compression config
50
42
  let compression_value: Value = config_value.funcall("compression", ())?;
51
43
  let compression = if compression_value.is_nil() {
52
44
  None
@@ -63,7 +55,6 @@ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<ServerC
63
55
  })
64
56
  };
65
57
 
66
- // Rate limit config
67
58
  let rate_limit_value: Value = config_value.funcall("rate_limit", ())?;
68
59
  let rate_limit = if rate_limit_value.is_nil() {
69
60
  None
@@ -78,7 +69,6 @@ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<ServerC
78
69
  })
79
70
  };
80
71
 
81
- // JWT auth config
82
72
  let jwt_auth_value: Value = config_value.funcall("jwt_auth", ())?;
83
73
  let jwt_auth = if jwt_auth_value.is_nil() {
84
74
  None
@@ -107,7 +97,6 @@ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<ServerC
107
97
  })
108
98
  };
109
99
 
110
- // API key auth config
111
100
  let api_key_auth_value: Value = config_value.funcall("api_key_auth", ())?;
112
101
  let api_key_auth = if api_key_auth_value.is_nil() {
113
102
  None
@@ -117,7 +106,6 @@ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<ServerC
117
106
  Some(ApiKeyConfig { keys, header_name })
118
107
  };
119
108
 
120
- // Static files config
121
109
  let static_files_value: Value = config_value.funcall("static_files", ())?;
122
110
  let static_files_array = RArray::from_value(static_files_value)
123
111
  .ok_or_else(|| Error::new(ruby.exception_type_error(), "static_files must be an Array"))?;
@@ -142,7 +130,6 @@ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<ServerC
142
130
  });
143
131
  }
144
132
 
145
- // OpenAPI config
146
133
  let openapi_value: Value = config_value.funcall("openapi", ())?;
147
134
  let openapi = if openapi_value.is_nil() {
148
135
  None
@@ -160,9 +147,80 @@ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<ServerC
160
147
  let redoc_path: String = openapi_value.funcall("redoc_path", ())?;
161
148
  let openapi_json_path: String = openapi_value.funcall("openapi_json_path", ())?;
162
149
 
163
- let contact = extract_contact_info(ruby, &openapi_value)?;
164
- let license = extract_license_info(ruby, &openapi_value)?;
165
- let servers = extract_servers(ruby, &openapi_value)?;
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
+ }
166
224
 
167
225
  let security_schemes = HashMap::new();
168
226
 
@@ -181,7 +239,7 @@ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<ServerC
181
239
  })
182
240
  };
183
241
 
184
- Ok(ServerConfig {
242
+ Ok(spikard_http::ServerConfig {
185
243
  host,
186
244
  port: port as u16,
187
245
  workers,
@@ -196,99 +254,32 @@ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<ServerC
196
254
  graceful_shutdown,
197
255
  shutdown_timeout,
198
256
  background_tasks: spikard_http::BackgroundTaskConfig::default(),
257
+ enable_http_trace: false,
199
258
  openapi,
259
+ jsonrpc: None,
200
260
  lifecycle_hooks: None,
201
- #[cfg(feature = "di")]
202
261
  di_container: None,
203
262
  })
204
263
  }
205
264
 
206
- /// Extract contact information from OpenAPI config.
207
- fn extract_contact_info(_ruby: &Ruby, openapi_value: &Value) -> Result<Option<ContactInfo>, Error> {
208
- let contact_value: Value = openapi_value.funcall("contact", ())?;
209
- if contact_value.is_nil() {
210
- return Ok(None);
265
+ /// Helper to extract an optional string from a Ruby Hash
266
+ pub fn get_optional_string_from_hash(hash: RHash, key: &str) -> Result<Option<String>, Error> {
267
+ match hash.get(String::from(key)) {
268
+ Some(v) if !v.is_nil() => Ok(Some(String::try_convert(v)?)),
269
+ _ => Ok(None),
211
270
  }
212
-
213
- if let Some(contact_hash) = RHash::from_value(contact_value) {
214
- let name = get_optional_string_from_hash(contact_hash, "name")?;
215
- let email = get_optional_string_from_hash(contact_hash, "email")?;
216
- let url = get_optional_string_from_hash(contact_hash, "url")?;
217
- return Ok(Some(ContactInfo { name, email, url }));
218
- }
219
-
220
- let name_value: Value = contact_value.funcall("name", ())?;
221
- let email_value: Value = contact_value.funcall("email", ())?;
222
- let url_value: Value = contact_value.funcall("url", ())?;
223
- Ok(Some(ContactInfo {
224
- name: if name_value.is_nil() {
225
- None
226
- } else {
227
- Some(String::try_convert(name_value)?)
228
- },
229
- email: if email_value.is_nil() {
230
- None
231
- } else {
232
- Some(String::try_convert(email_value)?)
233
- },
234
- url: if url_value.is_nil() {
235
- None
236
- } else {
237
- Some(String::try_convert(url_value)?)
238
- },
239
- }))
240
- }
241
-
242
- /// Extract license information from OpenAPI config.
243
- fn extract_license_info(ruby: &Ruby, openapi_value: &Value) -> Result<Option<LicenseInfo>, Error> {
244
- let license_value: Value = openapi_value.funcall("license", ())?;
245
- if license_value.is_nil() {
246
- return Ok(None);
247
- }
248
-
249
- if let Some(license_hash) = RHash::from_value(license_value) {
250
- let name = get_required_string_from_hash(license_hash, "name", ruby)?;
251
- let url = get_optional_string_from_hash(license_hash, "url")?;
252
- return Ok(Some(LicenseInfo { name, url }));
253
- }
254
-
255
- let name: String = license_value.funcall("name", ())?;
256
- let url_value: Value = license_value.funcall("url", ())?;
257
- let url = if url_value.is_nil() {
258
- None
259
- } else {
260
- Some(String::try_convert(url_value)?)
261
- };
262
- Ok(Some(LicenseInfo { name, url }))
263
271
  }
264
272
 
265
- /// Extract server information from OpenAPI config.
266
- fn extract_servers(ruby: &Ruby, openapi_value: &Value) -> Result<Vec<ServerInfo>, Error> {
267
- let servers_value: Value = openapi_value.funcall("servers", ())?;
268
- let servers_array = RArray::from_value(servers_value)
269
- .ok_or_else(|| Error::new(ruby.exception_type_error(), "servers must be an Array"))?;
270
-
271
- let mut servers = Vec::new();
272
- for i in 0..servers_array.len() {
273
- let server_value = servers_array.entry::<Value>(i as isize)?;
274
-
275
- let (url, description) = if let Some(server_hash) = RHash::from_value(server_value) {
276
- let url = get_required_string_from_hash(server_hash, "url", ruby)?;
277
- let description = get_optional_string_from_hash(server_hash, "description")?;
278
- (url, description)
279
- } else {
280
- let url: String = server_value.funcall("url", ())?;
281
- let description_value: Value = server_value.funcall("description", ())?;
282
- let description = if description_value.is_nil() {
283
- None
284
- } else {
285
- Some(String::try_convert(description_value)?)
286
- };
287
- (url, description)
288
- };
289
-
290
- servers.push(ServerInfo { url, description });
273
+ /// Helper to extract a required string from a Ruby Hash
274
+ pub fn get_required_string_from_hash(hash: RHash, key: &str, ruby: &Ruby) -> Result<String, Error> {
275
+ let value = hash
276
+ .get(String::from(key))
277
+ .ok_or_else(|| Error::new(ruby.exception_arg_error(), format!("missing required key '{}'", key)))?;
278
+ if value.is_nil() {
279
+ return Err(Error::new(
280
+ ruby.exception_arg_error(),
281
+ format!("key '{}' cannot be nil", key),
282
+ ));
291
283
  }
292
-
293
- Ok(servers)
284
+ String::try_convert(value)
294
285
  }