spikard 0.5.0 → 0.6.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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +674 -674
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +13 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +405 -405
- data/lib/spikard/background.rb +27 -27
- data/lib/spikard/config.rb +396 -396
- data/lib/spikard/converters.rb +13 -13
- data/lib/spikard/handler_wrapper.rb +113 -113
- data/lib/spikard/provide.rb +214 -214
- data/lib/spikard/response.rb +173 -173
- data/lib/spikard/schema.rb +243 -243
- data/lib/spikard/sse.rb +111 -111
- data/lib/spikard/streaming_response.rb +44 -44
- data/lib/spikard/testing.rb +256 -256
- data/lib/spikard/upload_file.rb +131 -131
- data/lib/spikard/version.rb +5 -5
- data/lib/spikard/websocket.rb +59 -59
- data/lib/spikard.rb +43 -43
- data/sig/spikard.rbs +366 -366
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -63
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -132
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -752
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -194
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -246
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -401
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -238
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -24
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -292
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -616
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -305
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -248
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -351
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -454
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -383
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -280
- data/vendor/crates/spikard-core/Cargo.toml +40 -40
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
- data/vendor/crates/spikard-core/src/debug.rs +127 -127
- data/vendor/crates/spikard-core/src/di/container.rs +702 -702
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
- data/vendor/crates/spikard-core/src/di/error.rs +118 -118
- data/vendor/crates/spikard-core/src/di/factory.rs +534 -534
- data/vendor/crates/spikard-core/src/di/graph.rs +506 -506
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +405 -405
- data/vendor/crates/spikard-core/src/di/value.rs +281 -281
- data/vendor/crates/spikard-core/src/errors.rs +69 -69
- data/vendor/crates/spikard-core/src/http.rs +415 -415
- data/vendor/crates/spikard-core/src/lib.rs +29 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -1186
- data/vendor/crates/spikard-core/src/metadata.rs +389 -389
- data/vendor/crates/spikard-core/src/parameters.rs +2525 -2525
- data/vendor/crates/spikard-core/src/problem.rs +344 -344
- data/vendor/crates/spikard-core/src/request_data.rs +1154 -1154
- data/vendor/crates/spikard-core/src/router.rs +510 -510
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +696 -688
- data/vendor/crates/spikard-core/src/validation/mod.rs +457 -457
- data/vendor/crates/spikard-http/Cargo.toml +62 -64
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -148
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -92
- data/vendor/crates/spikard-http/src/auth.rs +296 -296
- data/vendor/crates/spikard-http/src/background.rs +1860 -1860
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
- data/vendor/crates/spikard-http/src/cors.rs +1005 -1005
- data/vendor/crates/spikard-http/src/debug.rs +128 -128
- data/vendor/crates/spikard-http/src/di_handler.rs +1668 -1668
- data/vendor/crates/spikard-http/src/handler_response.rs +901 -901
- data/vendor/crates/spikard-http/src/handler_trait.rs +838 -830
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -290
- data/vendor/crates/spikard-http/src/lib.rs +534 -534
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -230
- data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -1193
- data/vendor/crates/spikard-http/src/middleware/mod.rs +560 -540
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -912
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -513
- data/vendor/crates/spikard-http/src/middleware/validation.rs +768 -735
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -535
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -1363
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -665
- data/vendor/crates/spikard-http/src/query_parser.rs +793 -793
- data/vendor/crates/spikard-http/src/response.rs +720 -720
- data/vendor/crates/spikard-http/src/server/handler.rs +1650 -1650
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -234
- data/vendor/crates/spikard-http/src/server/mod.rs +1593 -1502
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +789 -770
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +629 -599
- data/vendor/crates/spikard-http/src/sse.rs +1409 -1409
- data/vendor/crates/spikard-http/src/testing/form.rs +52 -52
- data/vendor/crates/spikard-http/src/testing/multipart.rs +64 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +311 -283
- data/vendor/crates/spikard-http/src/testing.rs +406 -377
- data/vendor/crates/spikard-http/src/websocket.rs +1404 -1375
- data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -832
- data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -309
- data/vendor/crates/spikard-http/tests/common/mod.rs +26 -26
- data/vendor/crates/spikard-http/tests/di_integration.rs +192 -192
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -5
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -1093
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -656
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -314
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -620
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -663
- data/vendor/crates/spikard-rb/Cargo.toml +48 -48
- data/vendor/crates/spikard-rb/build.rs +199 -199
- data/vendor/crates/spikard-rb/src/background.rs +63 -63
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/config/server_config.rs +285 -285
- data/vendor/crates/spikard-rb/src/conversion.rs +554 -554
- data/vendor/crates/spikard-rb/src/di/builder.rs +100 -100
- data/vendor/crates/spikard-rb/src/di/mod.rs +375 -375
- data/vendor/crates/spikard-rb/src/handler.rs +618 -618
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -3
- data/vendor/crates/spikard-rb/src/lib.rs +1806 -1810
- data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -275
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +442 -447
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -324
- data/vendor/crates/spikard-rb/src/server.rs +305 -308
- data/vendor/crates/spikard-rb/src/sse.rs +231 -231
- data/vendor/crates/spikard-rb/src/testing/client.rs +538 -551
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -7
- data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -143
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +608 -635
- data/vendor/crates/spikard-rb/src/websocket.rs +377 -374
- metadata +15 -1
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "spikard-rb"
|
|
3
|
-
version = "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", "embed"] }
|
|
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
|
-
spikard-rb-macros = { path = "../spikard-rb-macros" }
|
|
27
|
-
axum = { version = "0.8", features = ["multipart", "ws"] }
|
|
28
|
-
axum-test = "18"
|
|
29
|
-
bytes = "1.11"
|
|
30
|
-
cookie = "0.18"
|
|
31
|
-
tokio = { version = "1", features = ["full"] }
|
|
32
|
-
tungstenite = "0.
|
|
33
|
-
tracing = "0.1"
|
|
34
|
-
serde_qs = "0.15"
|
|
35
|
-
urlencoding = "2.1"
|
|
36
|
-
once_cell = "1.21"
|
|
37
|
-
async-stream = "0.3"
|
|
38
|
-
http = "1.4"
|
|
39
|
-
paste = "1.0.15"
|
|
40
|
-
rb-sys = "0"
|
|
41
|
-
url = "2.5"
|
|
42
|
-
|
|
43
|
-
[features]
|
|
44
|
-
default = ["di"]
|
|
45
|
-
di = ["spikard-core/di", "spikard-http/di"]
|
|
46
|
-
|
|
47
|
-
[build-dependencies]
|
|
48
|
-
rb-sys-build = "0.9"
|
|
1
|
+
[package]
|
|
2
|
+
name = "spikard-rb"
|
|
3
|
+
version = "0.6.1"
|
|
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", "embed"] }
|
|
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
|
+
spikard-rb-macros = { path = "../spikard-rb-macros" }
|
|
27
|
+
axum = { version = "0.8", features = ["multipart", "ws"] }
|
|
28
|
+
axum-test = "18"
|
|
29
|
+
bytes = "1.11"
|
|
30
|
+
cookie = "0.18"
|
|
31
|
+
tokio = { version = "1", features = ["full"] }
|
|
32
|
+
tungstenite = "0.28"
|
|
33
|
+
tracing = "0.1"
|
|
34
|
+
serde_qs = "0.15"
|
|
35
|
+
urlencoding = "2.1"
|
|
36
|
+
once_cell = "1.21"
|
|
37
|
+
async-stream = "0.3"
|
|
38
|
+
http = "1.4"
|
|
39
|
+
paste = "1.0.15"
|
|
40
|
+
rb-sys = "0"
|
|
41
|
+
url = "2.5"
|
|
42
|
+
|
|
43
|
+
[features]
|
|
44
|
+
default = ["di"]
|
|
45
|
+
di = ["spikard-core/di", "spikard-http/di"]
|
|
46
|
+
|
|
47
|
+
[build-dependencies]
|
|
48
|
+
rb-sys-build = "0.9"
|
|
@@ -1,199 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,63 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,5 +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
|
+
//! Server configuration extraction from Ruby objects.
|
|
2
|
+
|
|
3
|
+
pub mod server_config;
|
|
4
|
+
|
|
5
|
+
pub use server_config::extract_server_config;
|