spikard 0.4.0-arm64-darwin-23
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,427 @@
|
|
|
1
|
+
//! Route metadata extraction and building from Ruby objects.
|
|
2
|
+
//!
|
|
3
|
+
//! This module handles converting Ruby route definitions into structured
|
|
4
|
+
//! RouteMetadata that can be used by the Rust HTTP server.
|
|
5
|
+
|
|
6
|
+
use magnus::prelude::*;
|
|
7
|
+
use magnus::{Error, RArray, RHash, RString, Ruby, TryConvert, Value, r_hash::ForEach};
|
|
8
|
+
use serde_json::{Map as JsonMap, Value as JsonValue};
|
|
9
|
+
use spikard_http::{Route, RouteMetadata, SchemaRegistry};
|
|
10
|
+
|
|
11
|
+
/// Build route metadata from Ruby parameters
|
|
12
|
+
#[allow(clippy::too_many_arguments)]
|
|
13
|
+
pub fn build_route_metadata(
|
|
14
|
+
ruby: &Ruby,
|
|
15
|
+
method: String,
|
|
16
|
+
path: String,
|
|
17
|
+
handler_name: Option<String>,
|
|
18
|
+
request_schema_value: Value,
|
|
19
|
+
response_schema_value: Value,
|
|
20
|
+
parameter_schema_value: Value,
|
|
21
|
+
file_params_value: Value,
|
|
22
|
+
is_async: bool,
|
|
23
|
+
cors_value: Value,
|
|
24
|
+
body_param_name: Option<String>,
|
|
25
|
+
handler_value: Value,
|
|
26
|
+
) -> Result<Value, Error> {
|
|
27
|
+
let normalized_path = normalize_path_for_route(&path);
|
|
28
|
+
let final_handler_name = handler_name.unwrap_or_else(|| default_handler_name(&method, &normalized_path));
|
|
29
|
+
|
|
30
|
+
let json_module = ruby
|
|
31
|
+
.class_object()
|
|
32
|
+
.const_get("JSON")
|
|
33
|
+
.map_err(|_| Error::new(ruby.exception_runtime_error(), "JSON module not available"))?;
|
|
34
|
+
|
|
35
|
+
let request_schema = if request_schema_value.is_nil() {
|
|
36
|
+
None
|
|
37
|
+
} else {
|
|
38
|
+
Some(ruby_value_to_json(ruby, json_module, request_schema_value)?)
|
|
39
|
+
};
|
|
40
|
+
let response_schema = if response_schema_value.is_nil() {
|
|
41
|
+
None
|
|
42
|
+
} else {
|
|
43
|
+
Some(ruby_value_to_json(ruby, json_module, response_schema_value)?)
|
|
44
|
+
};
|
|
45
|
+
let parameter_schema = if parameter_schema_value.is_nil() {
|
|
46
|
+
None
|
|
47
|
+
} else {
|
|
48
|
+
Some(ruby_value_to_json(ruby, json_module, parameter_schema_value)?)
|
|
49
|
+
};
|
|
50
|
+
let file_params = if file_params_value.is_nil() {
|
|
51
|
+
None
|
|
52
|
+
} else {
|
|
53
|
+
Some(ruby_value_to_json(ruby, json_module, file_params_value)?)
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let cors = parse_cors_config(ruby, cors_value)?;
|
|
57
|
+
let handler_dependencies = extract_handler_dependencies_from_ruby(ruby, handler_value)?;
|
|
58
|
+
|
|
59
|
+
#[cfg(feature = "di")]
|
|
60
|
+
let handler_deps_option = if handler_dependencies.is_empty() {
|
|
61
|
+
None
|
|
62
|
+
} else {
|
|
63
|
+
Some(handler_dependencies.clone())
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let mut metadata = RouteMetadata {
|
|
67
|
+
method,
|
|
68
|
+
path: normalized_path,
|
|
69
|
+
handler_name: final_handler_name,
|
|
70
|
+
request_schema,
|
|
71
|
+
response_schema,
|
|
72
|
+
parameter_schema,
|
|
73
|
+
file_params,
|
|
74
|
+
is_async,
|
|
75
|
+
cors,
|
|
76
|
+
body_param_name,
|
|
77
|
+
#[cfg(feature = "di")]
|
|
78
|
+
handler_dependencies: handler_deps_option,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Validate schemas and parameter validator during build to fail fast
|
|
82
|
+
let registry = SchemaRegistry::new();
|
|
83
|
+
let route = Route::from_metadata(metadata.clone(), ®istry).map_err(|err| {
|
|
84
|
+
Error::new(
|
|
85
|
+
ruby.exception_runtime_error(),
|
|
86
|
+
format!("Failed to build route metadata: {err}"),
|
|
87
|
+
)
|
|
88
|
+
})?;
|
|
89
|
+
|
|
90
|
+
if let Some(validator) = route.parameter_validator.as_ref() {
|
|
91
|
+
metadata.parameter_schema = Some(validator.schema().clone());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
route_metadata_to_ruby(ruby, &metadata)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Convert a RouteMetadata to a Ruby hash
|
|
98
|
+
pub fn route_metadata_to_ruby(ruby: &Ruby, metadata: &RouteMetadata) -> Result<Value, Error> {
|
|
99
|
+
let hash = ruby.hash_new();
|
|
100
|
+
|
|
101
|
+
hash.aset(ruby.to_symbol("method"), ruby.str_new(&metadata.method))?;
|
|
102
|
+
hash.aset(ruby.to_symbol("path"), ruby.str_new(&metadata.path))?;
|
|
103
|
+
hash.aset(ruby.to_symbol("handler_name"), ruby.str_new(&metadata.handler_name))?;
|
|
104
|
+
let is_async_val: Value = if metadata.is_async {
|
|
105
|
+
ruby.qtrue().as_value()
|
|
106
|
+
} else {
|
|
107
|
+
ruby.qfalse().as_value()
|
|
108
|
+
};
|
|
109
|
+
hash.aset(ruby.to_symbol("is_async"), is_async_val)?;
|
|
110
|
+
|
|
111
|
+
hash.aset(
|
|
112
|
+
ruby.to_symbol("request_schema"),
|
|
113
|
+
option_json_to_ruby(ruby, &metadata.request_schema)?,
|
|
114
|
+
)?;
|
|
115
|
+
hash.aset(
|
|
116
|
+
ruby.to_symbol("response_schema"),
|
|
117
|
+
option_json_to_ruby(ruby, &metadata.response_schema)?,
|
|
118
|
+
)?;
|
|
119
|
+
hash.aset(
|
|
120
|
+
ruby.to_symbol("parameter_schema"),
|
|
121
|
+
option_json_to_ruby(ruby, &metadata.parameter_schema)?,
|
|
122
|
+
)?;
|
|
123
|
+
hash.aset(
|
|
124
|
+
ruby.to_symbol("file_params"),
|
|
125
|
+
option_json_to_ruby(ruby, &metadata.file_params)?,
|
|
126
|
+
)?;
|
|
127
|
+
hash.aset(
|
|
128
|
+
ruby.to_symbol("body_param_name"),
|
|
129
|
+
metadata
|
|
130
|
+
.body_param_name
|
|
131
|
+
.as_ref()
|
|
132
|
+
.map(|s| ruby.str_new(s).as_value())
|
|
133
|
+
.unwrap_or_else(|| ruby.qnil().as_value()),
|
|
134
|
+
)?;
|
|
135
|
+
|
|
136
|
+
hash.aset(ruby.to_symbol("cors"), cors_to_ruby(ruby, &metadata.cors)?)?;
|
|
137
|
+
|
|
138
|
+
#[cfg(feature = "di")]
|
|
139
|
+
{
|
|
140
|
+
if let Some(deps) = &metadata.handler_dependencies {
|
|
141
|
+
let array = ruby.ary_new();
|
|
142
|
+
for dep in deps {
|
|
143
|
+
array.push(ruby.str_new(dep))?;
|
|
144
|
+
}
|
|
145
|
+
hash.aset(ruby.to_symbol("handler_dependencies"), array)?;
|
|
146
|
+
} else {
|
|
147
|
+
hash.aset(ruby.to_symbol("handler_dependencies"), ruby.qnil())?;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Ok(hash.as_value())
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Normalize path for routes (convert :param to {param})
|
|
155
|
+
pub fn normalize_path_for_route(path: &str) -> String {
|
|
156
|
+
let has_trailing_slash = path.ends_with('/');
|
|
157
|
+
let segments = path.split('/').map(|segment| {
|
|
158
|
+
if let Some(stripped) = segment.strip_prefix(':') {
|
|
159
|
+
format!("{{{}}}", stripped)
|
|
160
|
+
} else {
|
|
161
|
+
segment.to_string()
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
let normalized = segments.collect::<Vec<_>>().join("/");
|
|
166
|
+
if has_trailing_slash && !normalized.ends_with('/') {
|
|
167
|
+
format!("{normalized}/")
|
|
168
|
+
} else {
|
|
169
|
+
normalized
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Generate default handler name from method and path
|
|
174
|
+
pub fn default_handler_name(method: &str, path: &str) -> String {
|
|
175
|
+
let normalized_path: String = path
|
|
176
|
+
.chars()
|
|
177
|
+
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
|
|
178
|
+
.collect();
|
|
179
|
+
let trimmed = normalized_path.trim_matches('_');
|
|
180
|
+
let final_segment = if trimmed.is_empty() { "root" } else { trimmed };
|
|
181
|
+
format!("{}_{}", method.to_ascii_lowercase(), final_segment)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Extract handler dependencies from a Ruby handler callable
|
|
185
|
+
pub fn extract_handler_dependencies_from_ruby(_ruby: &Ruby, handler_value: Value) -> Result<Vec<String>, Error> {
|
|
186
|
+
if handler_value.is_nil() {
|
|
187
|
+
return Ok(Vec::new());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let params_value: Value = handler_value.funcall("parameters", ())?;
|
|
191
|
+
let params = RArray::try_convert(params_value)?;
|
|
192
|
+
|
|
193
|
+
let mut dependencies = Vec::new();
|
|
194
|
+
for i in 0..params.len() {
|
|
195
|
+
let entry: Value = params.entry(i as isize)?;
|
|
196
|
+
if let Some(pair) = RArray::from_value(entry) {
|
|
197
|
+
if pair.len() < 2 {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let kind_val: Value = pair.entry(0)?;
|
|
202
|
+
let name_val: Value = pair.entry(1)?;
|
|
203
|
+
|
|
204
|
+
let kind_symbol: magnus::Symbol = magnus::Symbol::try_convert(kind_val)?;
|
|
205
|
+
let kind_name = kind_symbol.name().unwrap_or_default();
|
|
206
|
+
|
|
207
|
+
if kind_name == "key" || kind_name == "keyreq" {
|
|
208
|
+
if let Ok(sym) = magnus::Symbol::try_convert(name_val) {
|
|
209
|
+
if let Ok(name) = sym.name() {
|
|
210
|
+
dependencies.push(name.to_string());
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
dependencies.push(String::try_convert(name_val)?);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Ok(dependencies)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Parse CORS configuration from Ruby value
|
|
223
|
+
pub fn parse_cors_config(ruby: &Ruby, value: Value) -> Result<Option<spikard_http::CorsConfig>, Error> {
|
|
224
|
+
if value.is_nil() {
|
|
225
|
+
return Ok(None);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let hash = RHash::try_convert(value)?;
|
|
229
|
+
|
|
230
|
+
let allowed_origins = hash
|
|
231
|
+
.get(ruby.to_symbol("allowed_origins"))
|
|
232
|
+
.and_then(|v| Vec::<String>::try_convert(v).ok())
|
|
233
|
+
.unwrap_or_default();
|
|
234
|
+
let allowed_methods = hash
|
|
235
|
+
.get(ruby.to_symbol("allowed_methods"))
|
|
236
|
+
.and_then(|v| Vec::<String>::try_convert(v).ok())
|
|
237
|
+
.unwrap_or_default();
|
|
238
|
+
let allowed_headers = hash
|
|
239
|
+
.get(ruby.to_symbol("allowed_headers"))
|
|
240
|
+
.and_then(|v| Vec::<String>::try_convert(v).ok())
|
|
241
|
+
.unwrap_or_default();
|
|
242
|
+
let expose_headers = hash
|
|
243
|
+
.get(ruby.to_symbol("expose_headers"))
|
|
244
|
+
.and_then(|v| Vec::<String>::try_convert(v).ok());
|
|
245
|
+
let max_age = hash
|
|
246
|
+
.get(ruby.to_symbol("max_age"))
|
|
247
|
+
.and_then(|v| i64::try_convert(v).ok())
|
|
248
|
+
.map(|v| v as u32);
|
|
249
|
+
let allow_credentials = hash
|
|
250
|
+
.get(ruby.to_symbol("allow_credentials"))
|
|
251
|
+
.and_then(|v| bool::try_convert(v).ok());
|
|
252
|
+
|
|
253
|
+
Ok(Some(spikard_http::CorsConfig {
|
|
254
|
+
allowed_origins,
|
|
255
|
+
allowed_methods,
|
|
256
|
+
allowed_headers,
|
|
257
|
+
expose_headers,
|
|
258
|
+
max_age,
|
|
259
|
+
allow_credentials,
|
|
260
|
+
}))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Convert an optional JSON value to Ruby
|
|
264
|
+
pub fn option_json_to_ruby(ruby: &Ruby, value: &Option<JsonValue>) -> Result<Value, Error> {
|
|
265
|
+
if let Some(json) = value {
|
|
266
|
+
json_to_ruby(ruby, json)
|
|
267
|
+
} else {
|
|
268
|
+
Ok(ruby.qnil().as_value())
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Convert CORS config to Ruby hash
|
|
273
|
+
pub fn cors_to_ruby(ruby: &Ruby, cors: &Option<spikard_http::CorsConfig>) -> Result<Value, Error> {
|
|
274
|
+
if let Some(cors_config) = cors {
|
|
275
|
+
let hash = ruby.hash_new();
|
|
276
|
+
let origins = cors_config
|
|
277
|
+
.allowed_origins
|
|
278
|
+
.iter()
|
|
279
|
+
.map(|s| JsonValue::String(s.clone()))
|
|
280
|
+
.collect();
|
|
281
|
+
hash.aset(
|
|
282
|
+
ruby.to_symbol("allowed_origins"),
|
|
283
|
+
json_to_ruby(ruby, &JsonValue::Array(origins))?,
|
|
284
|
+
)?;
|
|
285
|
+
let methods = cors_config
|
|
286
|
+
.allowed_methods
|
|
287
|
+
.iter()
|
|
288
|
+
.map(|s| JsonValue::String(s.clone()))
|
|
289
|
+
.collect();
|
|
290
|
+
hash.aset(
|
|
291
|
+
ruby.to_symbol("allowed_methods"),
|
|
292
|
+
json_to_ruby(ruby, &JsonValue::Array(methods))?,
|
|
293
|
+
)?;
|
|
294
|
+
let headers = cors_config
|
|
295
|
+
.allowed_headers
|
|
296
|
+
.iter()
|
|
297
|
+
.map(|s| JsonValue::String(s.clone()))
|
|
298
|
+
.collect();
|
|
299
|
+
hash.aset(
|
|
300
|
+
ruby.to_symbol("allowed_headers"),
|
|
301
|
+
json_to_ruby(ruby, &JsonValue::Array(headers))?,
|
|
302
|
+
)?;
|
|
303
|
+
hash.aset(
|
|
304
|
+
ruby.to_symbol("expose_headers"),
|
|
305
|
+
if let Some(expose_headers) = &cors_config.expose_headers {
|
|
306
|
+
let expose = expose_headers.iter().map(|s| JsonValue::String(s.clone())).collect();
|
|
307
|
+
json_to_ruby(ruby, &JsonValue::Array(expose))?
|
|
308
|
+
} else {
|
|
309
|
+
ruby.qnil().as_value()
|
|
310
|
+
},
|
|
311
|
+
)?;
|
|
312
|
+
hash.aset(
|
|
313
|
+
ruby.to_symbol("max_age"),
|
|
314
|
+
if let Some(max_age) = cors_config.max_age {
|
|
315
|
+
ruby.integer_from_i64(max_age as i64).as_value()
|
|
316
|
+
} else {
|
|
317
|
+
ruby.qnil().as_value()
|
|
318
|
+
},
|
|
319
|
+
)?;
|
|
320
|
+
hash.aset(
|
|
321
|
+
ruby.to_symbol("allow_credentials"),
|
|
322
|
+
if let Some(allow_creds) = cors_config.allow_credentials {
|
|
323
|
+
if allow_creds {
|
|
324
|
+
ruby.qtrue().as_value()
|
|
325
|
+
} else {
|
|
326
|
+
ruby.qfalse().as_value()
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
ruby.qnil().as_value()
|
|
330
|
+
},
|
|
331
|
+
)?;
|
|
332
|
+
Ok(hash.as_value())
|
|
333
|
+
} else {
|
|
334
|
+
Ok(ruby.qnil().as_value())
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// Convert Ruby value to JSON
|
|
339
|
+
pub fn ruby_value_to_json(ruby: &Ruby, _json_module: Value, value: Value) -> Result<JsonValue, Error> {
|
|
340
|
+
if value.is_nil() {
|
|
341
|
+
return Ok(JsonValue::Null);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if let Ok(boolean) = bool::try_convert(value) {
|
|
345
|
+
return Ok(JsonValue::Bool(boolean));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if let Ok(int_val) = i64::try_convert(value) {
|
|
349
|
+
return Ok(JsonValue::Number(int_val.into()));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if let Ok(float_val) = f64::try_convert(value)
|
|
353
|
+
&& let Some(num) = serde_json::Number::from_f64(float_val)
|
|
354
|
+
{
|
|
355
|
+
return Ok(JsonValue::Number(num));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if let Ok(str_val) = RString::try_convert(value) {
|
|
359
|
+
let slice = str_val.to_string()?;
|
|
360
|
+
return Ok(JsonValue::String(slice));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if let Some(array) = RArray::from_value(value) {
|
|
364
|
+
let mut items = Vec::with_capacity(array.len());
|
|
365
|
+
let slice = unsafe { array.as_slice() };
|
|
366
|
+
for elem in slice {
|
|
367
|
+
items.push(ruby_value_to_json(ruby, _json_module, *elem)?);
|
|
368
|
+
}
|
|
369
|
+
return Ok(JsonValue::Array(items));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if let Some(hash) = RHash::from_value(value) {
|
|
373
|
+
let mut map = JsonMap::new();
|
|
374
|
+
hash.foreach(|key: Value, val: Value| -> Result<ForEach, Error> {
|
|
375
|
+
let key_str: String = if let Ok(sym) = magnus::Symbol::try_convert(key) {
|
|
376
|
+
sym.name().map(|c| c.to_string()).unwrap_or_default()
|
|
377
|
+
} else {
|
|
378
|
+
String::try_convert(key)?
|
|
379
|
+
};
|
|
380
|
+
let json_val = ruby_value_to_json(ruby, _json_module, val)?;
|
|
381
|
+
map.insert(key_str, json_val);
|
|
382
|
+
Ok(ForEach::Continue)
|
|
383
|
+
})?;
|
|
384
|
+
return Ok(JsonValue::Object(map));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
Err(Error::new(
|
|
388
|
+
ruby.exception_arg_error(),
|
|
389
|
+
"Unsupported Ruby value type for JSON conversion",
|
|
390
|
+
))
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/// Convert JSON to Ruby value
|
|
394
|
+
pub fn json_to_ruby(ruby: &Ruby, value: &JsonValue) -> Result<Value, Error> {
|
|
395
|
+
match value {
|
|
396
|
+
JsonValue::Null => Ok(ruby.qnil().as_value()),
|
|
397
|
+
JsonValue::Bool(b) => Ok(if *b {
|
|
398
|
+
ruby.qtrue().as_value()
|
|
399
|
+
} else {
|
|
400
|
+
ruby.qfalse().as_value()
|
|
401
|
+
}),
|
|
402
|
+
JsonValue::Number(num) => {
|
|
403
|
+
if let Some(i) = num.as_i64() {
|
|
404
|
+
Ok(ruby.integer_from_i64(i).as_value())
|
|
405
|
+
} else if let Some(f) = num.as_f64() {
|
|
406
|
+
Ok(ruby.float_from_f64(f).as_value())
|
|
407
|
+
} else {
|
|
408
|
+
Ok(ruby.qnil().as_value())
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
JsonValue::String(str_val) => Ok(ruby.str_new(str_val).as_value()),
|
|
412
|
+
JsonValue::Array(items) => {
|
|
413
|
+
let array = ruby.ary_new();
|
|
414
|
+
for item in items {
|
|
415
|
+
array.push(json_to_ruby(ruby, item)?)?;
|
|
416
|
+
}
|
|
417
|
+
Ok(array.as_value())
|
|
418
|
+
}
|
|
419
|
+
JsonValue::Object(map) => {
|
|
420
|
+
let hash = ruby.hash_new();
|
|
421
|
+
for (key, item) in map {
|
|
422
|
+
hash.aset(ruby.str_new(key), json_to_ruby(ruby, item)?)?;
|
|
423
|
+
}
|
|
424
|
+
Ok(hash.as_value())
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|