spikard 0.2.0 → 0.2.5
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/README.md +2 -2
- data/ext/spikard_rb/Cargo.toml +3 -2
- data/lib/spikard/version.rb +1 -1
- data/vendor/bundle/ruby/3.3.0/gems/diff-lcs-1.6.2/mise.toml +5 -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/http.rs +153 -0
- data/vendor/crates/spikard-core/src/lib.rs +28 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
- data/vendor/crates/spikard-core/src/parameters.rs +719 -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.rs +699 -0
- data/vendor/crates/spikard-http/Cargo.toml +58 -0
- data/vendor/crates/spikard-http/src/auth.rs +247 -0
- data/vendor/crates/spikard-http/src/background.rs +249 -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 +423 -0
- data/vendor/crates/spikard-http/src/handler_response.rs +190 -0
- data/vendor/crates/spikard-http/src/handler_trait.rs +228 -0
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
- data/vendor/crates/spikard-http/src/lib.rs +529 -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 +86 -0
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +147 -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 +190 -0
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +308 -0
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +195 -0
- data/vendor/crates/spikard-http/src/parameters.rs +1 -0
- data/vendor/crates/spikard-http/src/problem.rs +1 -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/router.rs +1 -0
- data/vendor/crates/spikard-http/src/schema_registry.rs +1 -0
- data/vendor/crates/spikard-http/src/server/handler.rs +80 -0
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
- data/vendor/crates/spikard-http/src/server/mod.rs +805 -0
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +119 -0
- data/vendor/crates/spikard-http/src/sse.rs +447 -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/type_hints.rs +1 -0
- data/vendor/crates/spikard-http/src/validation.rs +1 -0
- data/vendor/crates/spikard-http/src/websocket.rs +324 -0
- data/vendor/crates/spikard-rb/Cargo.toml +42 -0
- data/vendor/crates/spikard-rb/build.rs +8 -0
- data/vendor/crates/spikard-rb/src/background.rs +63 -0
- data/vendor/crates/spikard-rb/src/config.rs +294 -0
- data/vendor/crates/spikard-rb/src/conversion.rs +392 -0
- data/vendor/crates/spikard-rb/src/di.rs +409 -0
- data/vendor/crates/spikard-rb/src/handler.rs +534 -0
- data/vendor/crates/spikard-rb/src/lib.rs +2020 -0
- data/vendor/crates/spikard-rb/src/lifecycle.rs +267 -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/test_client.rs +404 -0
- data/vendor/crates/spikard-rb/src/test_sse.rs +143 -0
- data/vendor/crates/spikard-rb/src/test_websocket.rs +221 -0
- data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
- metadata +81 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
//! Parameter extraction from routes and schemas for OpenAPI generation
|
|
2
|
+
|
|
3
|
+
use utoipa::openapi::RefOr;
|
|
4
|
+
use utoipa::openapi::path::Parameter;
|
|
5
|
+
use utoipa::openapi::path::{ParameterBuilder, ParameterIn};
|
|
6
|
+
|
|
7
|
+
/// Extract parameters from JSON Schema parameter_schema
|
|
8
|
+
pub fn extract_parameters_from_schema(
|
|
9
|
+
param_schema: &serde_json::Value,
|
|
10
|
+
route_path: &str,
|
|
11
|
+
) -> Result<Vec<RefOr<Parameter>>, String> {
|
|
12
|
+
let mut parameters = Vec::new();
|
|
13
|
+
|
|
14
|
+
let path_params = extract_path_param_names(route_path);
|
|
15
|
+
|
|
16
|
+
let properties = param_schema
|
|
17
|
+
.get("properties")
|
|
18
|
+
.and_then(|p| p.as_object())
|
|
19
|
+
.ok_or_else(|| "Parameter schema missing 'properties' field".to_string())?;
|
|
20
|
+
|
|
21
|
+
let required = param_schema
|
|
22
|
+
.get("required")
|
|
23
|
+
.and_then(|r| r.as_array())
|
|
24
|
+
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
|
|
25
|
+
.unwrap_or_default();
|
|
26
|
+
|
|
27
|
+
for (name, schema) in properties {
|
|
28
|
+
let is_required = required.contains(&name.as_str());
|
|
29
|
+
let param_in = if path_params.contains(&name.as_str()) {
|
|
30
|
+
ParameterIn::Path
|
|
31
|
+
} else {
|
|
32
|
+
ParameterIn::Query
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let openapi_schema = crate::openapi::schema_conversion::json_value_to_schema(schema)?;
|
|
36
|
+
|
|
37
|
+
let is_path_param = matches!(param_in, ParameterIn::Path);
|
|
38
|
+
|
|
39
|
+
let param = ParameterBuilder::new()
|
|
40
|
+
.name(name)
|
|
41
|
+
.parameter_in(param_in)
|
|
42
|
+
.required(if is_path_param || is_required {
|
|
43
|
+
utoipa::openapi::Required::True
|
|
44
|
+
} else {
|
|
45
|
+
utoipa::openapi::Required::False
|
|
46
|
+
})
|
|
47
|
+
.schema(Some(openapi_schema))
|
|
48
|
+
.build();
|
|
49
|
+
|
|
50
|
+
parameters.push(RefOr::T(param));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Ok(parameters)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Extract path parameter names from route pattern (e.g., "/users/{id}" -> ["id"])
|
|
57
|
+
pub fn extract_path_param_names(route: &str) -> Vec<&str> {
|
|
58
|
+
route
|
|
59
|
+
.split('/')
|
|
60
|
+
.filter_map(|segment| {
|
|
61
|
+
if segment.starts_with('{') && segment.ends_with('}') {
|
|
62
|
+
Some(&segment[1..segment.len() - 1])
|
|
63
|
+
} else {
|
|
64
|
+
None
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.collect()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[cfg(test)]
|
|
71
|
+
mod tests {
|
|
72
|
+
use super::*;
|
|
73
|
+
use serde_json::json;
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn test_extract_path_param_names() {
|
|
77
|
+
let names = extract_path_param_names("/users/{id}/posts/{post_id}");
|
|
78
|
+
assert_eq!(names, vec!["id", "post_id"]);
|
|
79
|
+
|
|
80
|
+
let names = extract_path_param_names("/users");
|
|
81
|
+
assert_eq!(names, Vec::<&str>::new());
|
|
82
|
+
|
|
83
|
+
let names = extract_path_param_names("/users/{user_id}");
|
|
84
|
+
assert_eq!(names, vec!["user_id"]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn test_extract_parameters_from_schema_path_params() {
|
|
89
|
+
let param_schema = json!({
|
|
90
|
+
"type": "object",
|
|
91
|
+
"properties": {
|
|
92
|
+
"user_id": { "type": "integer" },
|
|
93
|
+
"post_id": { "type": "integer" }
|
|
94
|
+
},
|
|
95
|
+
"required": ["user_id", "post_id"]
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let result = extract_parameters_from_schema(¶m_schema, "/users/{user_id}/posts/{post_id}");
|
|
99
|
+
assert!(result.is_ok());
|
|
100
|
+
|
|
101
|
+
let params = result.unwrap();
|
|
102
|
+
assert_eq!(params.len(), 2);
|
|
103
|
+
|
|
104
|
+
for param in params {
|
|
105
|
+
if let RefOr::T(p) = param {
|
|
106
|
+
assert!(matches!(p.parameter_in, ParameterIn::Path));
|
|
107
|
+
assert!(matches!(p.required, utoipa::openapi::Required::True));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn test_extract_parameters_from_schema_query_params() {
|
|
114
|
+
let param_schema = json!({
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"page": { "type": "integer" },
|
|
118
|
+
"limit": { "type": "integer" },
|
|
119
|
+
"search": { "type": "string" }
|
|
120
|
+
},
|
|
121
|
+
"required": ["page"]
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let result = extract_parameters_from_schema(¶m_schema, "/users");
|
|
125
|
+
assert!(result.is_ok());
|
|
126
|
+
|
|
127
|
+
let params = result.unwrap();
|
|
128
|
+
assert_eq!(params.len(), 3);
|
|
129
|
+
|
|
130
|
+
for param in ¶ms {
|
|
131
|
+
if let RefOr::T(p) = param {
|
|
132
|
+
assert!(matches!(p.parameter_in, ParameterIn::Query));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for param in params {
|
|
137
|
+
if let RefOr::T(p) = param {
|
|
138
|
+
if p.name == "page" {
|
|
139
|
+
assert!(matches!(p.required, utoipa::openapi::Required::True));
|
|
140
|
+
} else {
|
|
141
|
+
assert!(matches!(p.required, utoipa::openapi::Required::False));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[test]
|
|
148
|
+
fn test_extract_parameters_from_schema_mixed() {
|
|
149
|
+
let param_schema = json!({
|
|
150
|
+
"type": "object",
|
|
151
|
+
"properties": {
|
|
152
|
+
"user_id": { "type": "integer" },
|
|
153
|
+
"page": { "type": "integer" },
|
|
154
|
+
"limit": { "type": "integer" }
|
|
155
|
+
},
|
|
156
|
+
"required": ["user_id"]
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
let result = extract_parameters_from_schema(¶m_schema, "/users/{user_id}");
|
|
160
|
+
assert!(result.is_ok());
|
|
161
|
+
|
|
162
|
+
let params = result.unwrap();
|
|
163
|
+
assert_eq!(params.len(), 3);
|
|
164
|
+
|
|
165
|
+
for param in params {
|
|
166
|
+
if let RefOr::T(p) = param {
|
|
167
|
+
if p.name == "user_id" {
|
|
168
|
+
assert!(matches!(p.parameter_in, ParameterIn::Path));
|
|
169
|
+
assert!(matches!(p.required, utoipa::openapi::Required::True));
|
|
170
|
+
} else {
|
|
171
|
+
assert!(matches!(p.parameter_in, ParameterIn::Query));
|
|
172
|
+
assert!(matches!(p.required, utoipa::openapi::Required::False));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[test]
|
|
179
|
+
fn test_extract_parameters_error_on_missing_properties() {
|
|
180
|
+
let param_schema = json!({
|
|
181
|
+
"type": "object"
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
let result = extract_parameters_from_schema(¶m_schema, "/users");
|
|
185
|
+
assert!(result.is_err());
|
|
186
|
+
if let Err(err) = result {
|
|
187
|
+
assert!(err.contains("properties"));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
//! JSON Schema to OpenAPI schema conversion utilities
|
|
2
|
+
|
|
3
|
+
use utoipa::openapi::{RefOr, Schema};
|
|
4
|
+
|
|
5
|
+
/// Convert serde_json::Value (JSON Schema) to utoipa Schema
|
|
6
|
+
/// OpenAPI 3.1.0 is fully compatible with JSON Schema Draft 2020-12
|
|
7
|
+
pub fn json_value_to_schema(value: &serde_json::Value) -> Result<RefOr<Schema>, String> {
|
|
8
|
+
if let Some(type_str) = value.get("type").and_then(|t| t.as_str()) {
|
|
9
|
+
match type_str {
|
|
10
|
+
"object" => {
|
|
11
|
+
let mut object_schema = utoipa::openapi::ObjectBuilder::new();
|
|
12
|
+
|
|
13
|
+
if let Some(properties) = value.get("properties").and_then(|p| p.as_object()) {
|
|
14
|
+
for (prop_name, prop_schema) in properties {
|
|
15
|
+
let prop_openapi_schema = json_value_to_schema(prop_schema)?;
|
|
16
|
+
object_schema = object_schema.property(prop_name, prop_openapi_schema);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if let Some(required) = value.get("required").and_then(|r| r.as_array()) {
|
|
21
|
+
for field in required {
|
|
22
|
+
if let Some(field_name) = field.as_str() {
|
|
23
|
+
object_schema = object_schema.required(field_name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Ok(RefOr::T(Schema::Object(object_schema.build())))
|
|
29
|
+
}
|
|
30
|
+
"array" => {
|
|
31
|
+
let mut array_schema = utoipa::openapi::ArrayBuilder::new();
|
|
32
|
+
|
|
33
|
+
if let Some(items) = value.get("items") {
|
|
34
|
+
let items_schema = json_value_to_schema(items)?;
|
|
35
|
+
array_schema = array_schema.items(items_schema);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Ok(RefOr::T(Schema::Array(array_schema.build())))
|
|
39
|
+
}
|
|
40
|
+
"string" => {
|
|
41
|
+
let mut schema_type = utoipa::openapi::schema::Type::String;
|
|
42
|
+
|
|
43
|
+
if let Some(format) = value.get("format").and_then(|f| f.as_str()) {
|
|
44
|
+
match format {
|
|
45
|
+
"date-time" => schema_type = utoipa::openapi::schema::Type::String,
|
|
46
|
+
"date" => schema_type = utoipa::openapi::schema::Type::String,
|
|
47
|
+
"email" => schema_type = utoipa::openapi::schema::Type::String,
|
|
48
|
+
"uri" => schema_type = utoipa::openapi::schema::Type::String,
|
|
49
|
+
_ => {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Ok(RefOr::T(Schema::Object(
|
|
54
|
+
utoipa::openapi::ObjectBuilder::new().schema_type(schema_type).build(),
|
|
55
|
+
)))
|
|
56
|
+
}
|
|
57
|
+
"integer" => Ok(RefOr::T(Schema::Object(
|
|
58
|
+
utoipa::openapi::ObjectBuilder::new()
|
|
59
|
+
.schema_type(utoipa::openapi::schema::Type::Integer)
|
|
60
|
+
.build(),
|
|
61
|
+
))),
|
|
62
|
+
"number" => Ok(RefOr::T(Schema::Object(
|
|
63
|
+
utoipa::openapi::ObjectBuilder::new()
|
|
64
|
+
.schema_type(utoipa::openapi::schema::Type::Number)
|
|
65
|
+
.build(),
|
|
66
|
+
))),
|
|
67
|
+
"boolean" => Ok(RefOr::T(Schema::Object(
|
|
68
|
+
utoipa::openapi::ObjectBuilder::new()
|
|
69
|
+
.schema_type(utoipa::openapi::schema::Type::Boolean)
|
|
70
|
+
.build(),
|
|
71
|
+
))),
|
|
72
|
+
_ => Err(format!("Unsupported schema type: {}", type_str)),
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
Ok(RefOr::T(Schema::Object(utoipa::openapi::ObjectBuilder::new().build())))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Convert JSON Schema to OpenAPI RequestBody
|
|
80
|
+
pub fn json_schema_to_request_body(
|
|
81
|
+
schema: &serde_json::Value,
|
|
82
|
+
) -> Result<utoipa::openapi::request_body::RequestBody, String> {
|
|
83
|
+
use utoipa::openapi::content::ContentBuilder;
|
|
84
|
+
|
|
85
|
+
let openapi_schema = json_value_to_schema(schema)?;
|
|
86
|
+
|
|
87
|
+
let content = ContentBuilder::new().schema(Some(openapi_schema)).build();
|
|
88
|
+
|
|
89
|
+
let mut request_body = utoipa::openapi::request_body::RequestBody::new();
|
|
90
|
+
request_body.content.insert("application/json".to_string(), content);
|
|
91
|
+
|
|
92
|
+
request_body.required = Some(utoipa::openapi::Required::True);
|
|
93
|
+
|
|
94
|
+
Ok(request_body)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Convert JSON Schema to OpenAPI Response
|
|
98
|
+
pub fn json_schema_to_response(schema: &serde_json::Value) -> Result<utoipa::openapi::Response, String> {
|
|
99
|
+
use utoipa::openapi::content::ContentBuilder;
|
|
100
|
+
|
|
101
|
+
let openapi_schema = json_value_to_schema(schema)?;
|
|
102
|
+
|
|
103
|
+
let content = ContentBuilder::new().schema(Some(openapi_schema)).build();
|
|
104
|
+
|
|
105
|
+
let mut response = utoipa::openapi::Response::new("Successful response");
|
|
106
|
+
response.content.insert("application/json".to_string(), content);
|
|
107
|
+
|
|
108
|
+
Ok(response)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[cfg(test)]
|
|
112
|
+
mod tests {
|
|
113
|
+
use super::*;
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn test_json_value_to_schema_string() {
|
|
117
|
+
let schema_json = serde_json::json!({
|
|
118
|
+
"type": "string"
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
let result = json_value_to_schema(&schema_json);
|
|
122
|
+
assert!(result.is_ok());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[test]
|
|
126
|
+
fn test_json_value_to_schema_integer() {
|
|
127
|
+
let schema_json = serde_json::json!({
|
|
128
|
+
"type": "integer"
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
let result = json_value_to_schema(&schema_json);
|
|
132
|
+
assert!(result.is_ok());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn test_json_value_to_schema_number() {
|
|
137
|
+
let schema_json = serde_json::json!({
|
|
138
|
+
"type": "number"
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
let result = json_value_to_schema(&schema_json);
|
|
142
|
+
assert!(result.is_ok());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[test]
|
|
146
|
+
fn test_json_value_to_schema_boolean() {
|
|
147
|
+
let schema_json = serde_json::json!({
|
|
148
|
+
"type": "boolean"
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let result = json_value_to_schema(&schema_json);
|
|
152
|
+
assert!(result.is_ok());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[test]
|
|
156
|
+
fn test_json_value_to_schema_object() {
|
|
157
|
+
let schema_json = serde_json::json!({
|
|
158
|
+
"type": "object",
|
|
159
|
+
"properties": {
|
|
160
|
+
"name": { "type": "string" },
|
|
161
|
+
"age": { "type": "integer" }
|
|
162
|
+
},
|
|
163
|
+
"required": ["name"]
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
let result = json_value_to_schema(&schema_json);
|
|
167
|
+
assert!(result.is_ok());
|
|
168
|
+
|
|
169
|
+
if let Ok(RefOr::T(Schema::Object(obj))) = result {
|
|
170
|
+
assert!(obj.properties.contains_key("name"));
|
|
171
|
+
assert!(obj.properties.contains_key("age"));
|
|
172
|
+
assert!(obj.required.contains(&"name".to_string()));
|
|
173
|
+
} else {
|
|
174
|
+
panic!("Expected Object schema");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[test]
|
|
179
|
+
fn test_json_value_to_schema_array() {
|
|
180
|
+
let schema_json = serde_json::json!({
|
|
181
|
+
"type": "array",
|
|
182
|
+
"items": {
|
|
183
|
+
"type": "string"
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
let result = json_value_to_schema(&schema_json);
|
|
188
|
+
assert!(result.is_ok());
|
|
189
|
+
|
|
190
|
+
if let Ok(RefOr::T(Schema::Array(_))) = result {
|
|
191
|
+
} else {
|
|
192
|
+
panic!("Expected Array schema");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[test]
|
|
197
|
+
fn test_json_value_to_schema_nested_object() {
|
|
198
|
+
let schema_json = serde_json::json!({
|
|
199
|
+
"type": "object",
|
|
200
|
+
"properties": {
|
|
201
|
+
"user": {
|
|
202
|
+
"type": "object",
|
|
203
|
+
"properties": {
|
|
204
|
+
"name": { "type": "string" },
|
|
205
|
+
"email": { "type": "string" }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
let result = json_value_to_schema(&schema_json);
|
|
212
|
+
assert!(result.is_ok());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn test_json_schema_to_request_body() {
|
|
217
|
+
let schema_json = serde_json::json!({
|
|
218
|
+
"type": "object",
|
|
219
|
+
"properties": {
|
|
220
|
+
"title": { "type": "string" },
|
|
221
|
+
"count": { "type": "integer" }
|
|
222
|
+
},
|
|
223
|
+
"required": ["title"]
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
let result = json_schema_to_request_body(&schema_json);
|
|
227
|
+
assert!(result.is_ok());
|
|
228
|
+
|
|
229
|
+
let request_body = result.unwrap();
|
|
230
|
+
assert!(request_body.content.contains_key("application/json"));
|
|
231
|
+
assert!(matches!(request_body.required, Some(utoipa::openapi::Required::True)));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[test]
|
|
235
|
+
fn test_json_schema_to_request_body_array() {
|
|
236
|
+
let schema_json = serde_json::json!({
|
|
237
|
+
"type": "array",
|
|
238
|
+
"items": {
|
|
239
|
+
"type": "object",
|
|
240
|
+
"properties": {
|
|
241
|
+
"id": { "type": "integer" }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
let result = json_schema_to_request_body(&schema_json);
|
|
247
|
+
assert!(result.is_ok());
|
|
248
|
+
|
|
249
|
+
let request_body = result.unwrap();
|
|
250
|
+
assert!(request_body.content.contains_key("application/json"));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn test_json_schema_to_response() {
|
|
255
|
+
let schema_json = serde_json::json!({
|
|
256
|
+
"type": "object",
|
|
257
|
+
"properties": {
|
|
258
|
+
"id": { "type": "integer" },
|
|
259
|
+
"name": { "type": "string" }
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
let result = json_schema_to_response(&schema_json);
|
|
264
|
+
assert!(result.is_ok());
|
|
265
|
+
|
|
266
|
+
let response = result.unwrap();
|
|
267
|
+
assert!(response.content.contains_key("application/json"));
|
|
268
|
+
assert_eq!(response.description, "Successful response");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn test_json_schema_to_response_array() {
|
|
273
|
+
let schema_json = serde_json::json!({
|
|
274
|
+
"type": "array",
|
|
275
|
+
"items": {
|
|
276
|
+
"type": "string"
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
let result = json_schema_to_response(&schema_json);
|
|
281
|
+
assert!(result.is_ok());
|
|
282
|
+
|
|
283
|
+
let response = result.unwrap();
|
|
284
|
+
assert!(response.content.contains_key("application/json"));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#[test]
|
|
288
|
+
fn test_json_value_to_schema_string_with_format() {
|
|
289
|
+
let schema_json = serde_json::json!({
|
|
290
|
+
"type": "string",
|
|
291
|
+
"format": "date-time"
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
let result = json_value_to_schema(&schema_json);
|
|
295
|
+
assert!(result.is_ok());
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#[test]
|
|
299
|
+
fn test_json_schema_to_request_body_empty_object() {
|
|
300
|
+
let schema_json = serde_json::json!({
|
|
301
|
+
"type": "object",
|
|
302
|
+
"properties": {}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
let result = json_schema_to_request_body(&schema_json);
|
|
306
|
+
assert!(result.is_ok());
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
//! OpenAPI specification generation and assembly
|
|
2
|
+
|
|
3
|
+
use crate::RouteMetadata;
|
|
4
|
+
use utoipa::openapi::HttpMethod;
|
|
5
|
+
use utoipa::openapi::security::SecurityScheme;
|
|
6
|
+
use utoipa::openapi::{Components, Info, OpenApi, OpenApiBuilder, PathItem, Paths, RefOr, Response, Responses};
|
|
7
|
+
|
|
8
|
+
/// Convert route to OpenAPI PathItem
|
|
9
|
+
fn route_to_path_item(route: &RouteMetadata) -> Result<PathItem, String> {
|
|
10
|
+
let operation = route_to_operation(route)?;
|
|
11
|
+
|
|
12
|
+
let http_method = match route.method.to_uppercase().as_str() {
|
|
13
|
+
"GET" => HttpMethod::Get,
|
|
14
|
+
"POST" => HttpMethod::Post,
|
|
15
|
+
"PUT" => HttpMethod::Put,
|
|
16
|
+
"DELETE" => HttpMethod::Delete,
|
|
17
|
+
"PATCH" => HttpMethod::Patch,
|
|
18
|
+
"HEAD" => HttpMethod::Head,
|
|
19
|
+
"OPTIONS" => HttpMethod::Options,
|
|
20
|
+
_ => return Err(format!("Unsupported HTTP method: {}", route.method)),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let path_item = PathItem::new(http_method, operation);
|
|
24
|
+
|
|
25
|
+
Ok(path_item)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Convert route to OpenAPI Operation
|
|
29
|
+
fn route_to_operation(route: &RouteMetadata) -> Result<utoipa::openapi::path::Operation, String> {
|
|
30
|
+
let mut operation = utoipa::openapi::path::Operation::new();
|
|
31
|
+
|
|
32
|
+
if let Some(param_schema) = &route.parameter_schema {
|
|
33
|
+
let parameters =
|
|
34
|
+
crate::openapi::parameter_extraction::extract_parameters_from_schema(param_schema, &route.path)?;
|
|
35
|
+
if !parameters.is_empty() {
|
|
36
|
+
let unwrapped: Vec<_> = parameters
|
|
37
|
+
.into_iter()
|
|
38
|
+
.filter_map(|p| if let RefOr::T(param) = p { Some(param) } else { None })
|
|
39
|
+
.collect();
|
|
40
|
+
operation.parameters = Some(unwrapped);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if let Some(request_schema) = &route.request_schema {
|
|
45
|
+
let request_body = crate::openapi::schema_conversion::json_schema_to_request_body(request_schema)?;
|
|
46
|
+
operation.request_body = Some(request_body);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let mut responses = Responses::new();
|
|
50
|
+
if let Some(response_schema) = &route.response_schema {
|
|
51
|
+
let response = crate::openapi::schema_conversion::json_schema_to_response(response_schema)?;
|
|
52
|
+
responses.responses.insert("200".to_string(), RefOr::T(response));
|
|
53
|
+
} else {
|
|
54
|
+
responses
|
|
55
|
+
.responses
|
|
56
|
+
.insert("200".to_string(), RefOr::T(Response::new("Successful response")));
|
|
57
|
+
}
|
|
58
|
+
operation.responses = responses;
|
|
59
|
+
|
|
60
|
+
Ok(operation)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Assemble OpenAPI specification from routes with auto-detection of security schemes
|
|
64
|
+
pub fn assemble_openapi_spec(
|
|
65
|
+
routes: &[RouteMetadata],
|
|
66
|
+
config: &super::OpenApiConfig,
|
|
67
|
+
server_config: Option<&crate::ServerConfig>,
|
|
68
|
+
) -> Result<OpenApi, String> {
|
|
69
|
+
let mut info = Info::new(&config.title, &config.version);
|
|
70
|
+
if let Some(desc) = &config.description {
|
|
71
|
+
info.description = Some(desc.clone());
|
|
72
|
+
}
|
|
73
|
+
if let Some(contact_info) = &config.contact {
|
|
74
|
+
let mut contact = utoipa::openapi::Contact::default();
|
|
75
|
+
if let Some(name) = &contact_info.name {
|
|
76
|
+
contact.name = Some(name.clone());
|
|
77
|
+
}
|
|
78
|
+
if let Some(email) = &contact_info.email {
|
|
79
|
+
contact.email = Some(email.clone());
|
|
80
|
+
}
|
|
81
|
+
if let Some(url) = &contact_info.url {
|
|
82
|
+
contact.url = Some(url.clone());
|
|
83
|
+
}
|
|
84
|
+
info.contact = Some(contact);
|
|
85
|
+
}
|
|
86
|
+
if let Some(license_info) = &config.license {
|
|
87
|
+
let mut license = utoipa::openapi::License::new(&license_info.name);
|
|
88
|
+
if let Some(url) = &license_info.url {
|
|
89
|
+
license.url = Some(url.clone());
|
|
90
|
+
}
|
|
91
|
+
info.license = Some(license);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let servers = if config.servers.is_empty() {
|
|
95
|
+
None
|
|
96
|
+
} else {
|
|
97
|
+
Some(
|
|
98
|
+
config
|
|
99
|
+
.servers
|
|
100
|
+
.iter()
|
|
101
|
+
.map(|s| {
|
|
102
|
+
let mut server = utoipa::openapi::Server::new(&s.url);
|
|
103
|
+
if let Some(desc) = &s.description {
|
|
104
|
+
server.description = Some(desc.clone());
|
|
105
|
+
}
|
|
106
|
+
server
|
|
107
|
+
})
|
|
108
|
+
.collect(),
|
|
109
|
+
)
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
let mut paths = Paths::new();
|
|
113
|
+
for route in routes {
|
|
114
|
+
let path_item = route_to_path_item(route)?;
|
|
115
|
+
paths.paths.insert(route.path.clone(), path_item);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let mut components = Components::new();
|
|
119
|
+
let mut global_security = Vec::new();
|
|
120
|
+
|
|
121
|
+
if let Some(server_cfg) = server_config {
|
|
122
|
+
if let Some(_jwt_cfg) = &server_cfg.jwt_auth {
|
|
123
|
+
let jwt_scheme = SecurityScheme::Http(
|
|
124
|
+
utoipa::openapi::security::HttpBuilder::new()
|
|
125
|
+
.scheme(utoipa::openapi::security::HttpAuthScheme::Bearer)
|
|
126
|
+
.bearer_format("JWT")
|
|
127
|
+
.build(),
|
|
128
|
+
);
|
|
129
|
+
components.add_security_scheme("bearerAuth", jwt_scheme);
|
|
130
|
+
|
|
131
|
+
let security_req = utoipa::openapi::security::SecurityRequirement::new("bearerAuth", Vec::<String>::new());
|
|
132
|
+
global_security.push(security_req);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if let Some(api_key_cfg) = &server_cfg.api_key_auth {
|
|
136
|
+
use utoipa::openapi::security::ApiKey;
|
|
137
|
+
let api_key_scheme = SecurityScheme::ApiKey(ApiKey::Header(utoipa::openapi::security::ApiKeyValue::new(
|
|
138
|
+
&api_key_cfg.header_name,
|
|
139
|
+
)));
|
|
140
|
+
components.add_security_scheme("apiKeyAuth", api_key_scheme);
|
|
141
|
+
|
|
142
|
+
let security_req = utoipa::openapi::security::SecurityRequirement::new("apiKeyAuth", Vec::<String>::new());
|
|
143
|
+
global_security.push(security_req);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if !config.security_schemes.is_empty() {
|
|
148
|
+
for (name, scheme_info) in &config.security_schemes {
|
|
149
|
+
let scheme = crate::openapi::security_scheme_info_to_openapi(scheme_info);
|
|
150
|
+
components.add_security_scheme(name, scheme);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let mut openapi = OpenApiBuilder::new()
|
|
155
|
+
.info(info)
|
|
156
|
+
.paths(paths)
|
|
157
|
+
.components(Some(components))
|
|
158
|
+
.build();
|
|
159
|
+
|
|
160
|
+
if let Some(servers) = servers {
|
|
161
|
+
openapi.servers = Some(servers);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if !global_security.is_empty() {
|
|
165
|
+
openapi.security = Some(global_security);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
Ok(openapi)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[cfg(test)]
|
|
172
|
+
mod tests {
|
|
173
|
+
use super::*;
|
|
174
|
+
|
|
175
|
+
#[test]
|
|
176
|
+
fn test_route_to_path_item_get() {
|
|
177
|
+
let route = RouteMetadata {
|
|
178
|
+
method: "GET".to_string(),
|
|
179
|
+
path: "/users".to_string(),
|
|
180
|
+
handler_name: "list_users".to_string(),
|
|
181
|
+
request_schema: None,
|
|
182
|
+
response_schema: None,
|
|
183
|
+
parameter_schema: None,
|
|
184
|
+
file_params: None,
|
|
185
|
+
is_async: true,
|
|
186
|
+
cors: None,
|
|
187
|
+
body_param_name: None,
|
|
188
|
+
#[cfg(feature = "di")]
|
|
189
|
+
handler_dependencies: None,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
let result = route_to_path_item(&route);
|
|
193
|
+
assert!(result.is_ok());
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pub use spikard_core::parameters::*;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pub use spikard_core::problem::*;
|