spikard 0.4.0-x86_64-linux
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.
- checksums.yaml +7 -0
- data/LICENSE +1 -0
- data/README.md +659 -0
- data/ext/spikard_rb/Cargo.toml +17 -0
- data/ext/spikard_rb/extconf.rb +10 -0
- data/ext/spikard_rb/src/lib.rs +6 -0
- data/lib/spikard/app.rb +405 -0
- data/lib/spikard/background.rb +27 -0
- data/lib/spikard/config.rb +396 -0
- data/lib/spikard/converters.rb +13 -0
- data/lib/spikard/handler_wrapper.rb +113 -0
- data/lib/spikard/provide.rb +214 -0
- data/lib/spikard/response.rb +173 -0
- data/lib/spikard/schema.rb +243 -0
- data/lib/spikard/sse.rb +111 -0
- data/lib/spikard/streaming_response.rb +44 -0
- data/lib/spikard/testing.rb +221 -0
- data/lib/spikard/upload_file.rb +131 -0
- data/lib/spikard/version.rb +5 -0
- data/lib/spikard/websocket.rb +59 -0
- data/lib/spikard.rb +43 -0
- data/sig/spikard.rbs +366 -0
- data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +5 -0
- data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +2 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +139 -0
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +561 -0
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +403 -0
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +274 -0
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +25 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +298 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +637 -0
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +309 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +355 -0
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +502 -0
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +389 -0
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +413 -0
- data/vendor/crates/spikard-core/Cargo.toml +40 -0
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -0
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -0
- data/vendor/crates/spikard-core/src/debug.rs +63 -0
- data/vendor/crates/spikard-core/src/di/container.rs +726 -0
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -0
- data/vendor/crates/spikard-core/src/di/error.rs +118 -0
- data/vendor/crates/spikard-core/src/di/factory.rs +538 -0
- data/vendor/crates/spikard-core/src/di/graph.rs +545 -0
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -0
- data/vendor/crates/spikard-core/src/di/resolved.rs +411 -0
- data/vendor/crates/spikard-core/src/di/value.rs +283 -0
- data/vendor/crates/spikard-core/src/errors.rs +39 -0
- data/vendor/crates/spikard-core/src/http.rs +153 -0
- data/vendor/crates/spikard-core/src/lib.rs +29 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
- data/vendor/crates/spikard-core/src/metadata.rs +397 -0
- data/vendor/crates/spikard-core/src/parameters.rs +723 -0
- data/vendor/crates/spikard-core/src/problem.rs +310 -0
- data/vendor/crates/spikard-core/src/request_data.rs +189 -0
- data/vendor/crates/spikard-core/src/router.rs +249 -0
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -0
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -0
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +689 -0
- data/vendor/crates/spikard-core/src/validation/mod.rs +459 -0
- data/vendor/crates/spikard-http/Cargo.toml +58 -0
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +147 -0
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +91 -0
- data/vendor/crates/spikard-http/src/auth.rs +247 -0
- data/vendor/crates/spikard-http/src/background.rs +1562 -0
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -0
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -0
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -0
- data/vendor/crates/spikard-http/src/cors.rs +490 -0
- data/vendor/crates/spikard-http/src/debug.rs +63 -0
- data/vendor/crates/spikard-http/src/di_handler.rs +1878 -0
- data/vendor/crates/spikard-http/src/handler_response.rs +532 -0
- data/vendor/crates/spikard-http/src/handler_trait.rs +861 -0
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
- data/vendor/crates/spikard-http/src/lib.rs +524 -0
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -0
- data/vendor/crates/spikard-http/src/lifecycle.rs +428 -0
- data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -0
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +930 -0
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +541 -0
- data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -0
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -0
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -0
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +867 -0
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +678 -0
- data/vendor/crates/spikard-http/src/query_parser.rs +369 -0
- data/vendor/crates/spikard-http/src/response.rs +399 -0
- data/vendor/crates/spikard-http/src/server/handler.rs +1557 -0
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
- data/vendor/crates/spikard-http/src/server/mod.rs +806 -0
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +630 -0
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +497 -0
- data/vendor/crates/spikard-http/src/sse.rs +961 -0
- data/vendor/crates/spikard-http/src/testing/form.rs +14 -0
- data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -0
- data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -0
- data/vendor/crates/spikard-http/src/testing.rs +377 -0
- data/vendor/crates/spikard-http/src/websocket.rs +831 -0
- data/vendor/crates/spikard-http/tests/background_behavior.rs +918 -0
- data/vendor/crates/spikard-http/tests/common/handlers.rs +308 -0
- data/vendor/crates/spikard-http/tests/common/mod.rs +21 -0
- data/vendor/crates/spikard-http/tests/di_integration.rs +202 -0
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +4 -0
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1135 -0
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +688 -0
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +324 -0
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +728 -0
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +724 -0
- data/vendor/crates/spikard-rb/Cargo.toml +43 -0
- data/vendor/crates/spikard-rb/build.rs +199 -0
- data/vendor/crates/spikard-rb/src/background.rs +63 -0
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/config/server_config.rs +283 -0
- data/vendor/crates/spikard-rb/src/conversion.rs +459 -0
- data/vendor/crates/spikard-rb/src/di/builder.rs +105 -0
- data/vendor/crates/spikard-rb/src/di/mod.rs +413 -0
- data/vendor/crates/spikard-rb/src/handler.rs +612 -0
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
- data/vendor/crates/spikard-rb/src/lib.rs +1857 -0
- data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -0
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +427 -0
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +326 -0
- data/vendor/crates/spikard-rb/src/server.rs +283 -0
- data/vendor/crates/spikard-rb/src/sse.rs +231 -0
- data/vendor/crates/spikard-rb/src/testing/client.rs +404 -0
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
- data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -0
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +221 -0
- data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
- data/vendor/crates/spikard-rb/tests/magnus_ffi_tests.rs +14 -0
- 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,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
|
+
}
|