spikard 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -1
  3. data/ext/spikard_rb/Cargo.toml +3 -3
  4. data/lib/spikard/app.rb +61 -49
  5. data/lib/spikard/converters.rb +3 -75
  6. data/lib/spikard/handler_wrapper.rb +6 -9
  7. data/lib/spikard/provide.rb +14 -28
  8. data/lib/spikard/response.rb +75 -11
  9. data/lib/spikard/streaming_response.rb +24 -1
  10. data/lib/spikard/testing.rb +1 -1
  11. data/lib/spikard/version.rb +1 -1
  12. data/sig/spikard.rbs +14 -3
  13. data/vendor/bundle/ruby/3.3.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +2 -0
  14. metadata +3 -80
  15. data/vendor/crates/spikard-core/Cargo.toml +0 -40
  16. data/vendor/crates/spikard-core/src/bindings/mod.rs +0 -3
  17. data/vendor/crates/spikard-core/src/bindings/response.rs +0 -133
  18. data/vendor/crates/spikard-core/src/debug.rs +0 -63
  19. data/vendor/crates/spikard-core/src/di/container.rs +0 -726
  20. data/vendor/crates/spikard-core/src/di/dependency.rs +0 -273
  21. data/vendor/crates/spikard-core/src/di/error.rs +0 -118
  22. data/vendor/crates/spikard-core/src/di/factory.rs +0 -538
  23. data/vendor/crates/spikard-core/src/di/graph.rs +0 -545
  24. data/vendor/crates/spikard-core/src/di/mod.rs +0 -192
  25. data/vendor/crates/spikard-core/src/di/resolved.rs +0 -411
  26. data/vendor/crates/spikard-core/src/di/value.rs +0 -283
  27. data/vendor/crates/spikard-core/src/http.rs +0 -153
  28. data/vendor/crates/spikard-core/src/lib.rs +0 -28
  29. data/vendor/crates/spikard-core/src/lifecycle.rs +0 -422
  30. data/vendor/crates/spikard-core/src/parameters.rs +0 -719
  31. data/vendor/crates/spikard-core/src/problem.rs +0 -310
  32. data/vendor/crates/spikard-core/src/request_data.rs +0 -189
  33. data/vendor/crates/spikard-core/src/router.rs +0 -249
  34. data/vendor/crates/spikard-core/src/schema_registry.rs +0 -183
  35. data/vendor/crates/spikard-core/src/type_hints.rs +0 -304
  36. data/vendor/crates/spikard-core/src/validation.rs +0 -699
  37. data/vendor/crates/spikard-http/Cargo.toml +0 -58
  38. data/vendor/crates/spikard-http/src/auth.rs +0 -247
  39. data/vendor/crates/spikard-http/src/background.rs +0 -249
  40. data/vendor/crates/spikard-http/src/bindings/mod.rs +0 -3
  41. data/vendor/crates/spikard-http/src/bindings/response.rs +0 -1
  42. data/vendor/crates/spikard-http/src/body_metadata.rs +0 -8
  43. data/vendor/crates/spikard-http/src/cors.rs +0 -490
  44. data/vendor/crates/spikard-http/src/debug.rs +0 -63
  45. data/vendor/crates/spikard-http/src/di_handler.rs +0 -423
  46. data/vendor/crates/spikard-http/src/handler_response.rs +0 -190
  47. data/vendor/crates/spikard-http/src/handler_trait.rs +0 -228
  48. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +0 -284
  49. data/vendor/crates/spikard-http/src/lib.rs +0 -529
  50. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -149
  51. data/vendor/crates/spikard-http/src/lifecycle.rs +0 -428
  52. data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -285
  53. data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -86
  54. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -147
  55. data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -287
  56. data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
  57. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -190
  58. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -308
  59. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -195
  60. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  61. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  62. data/vendor/crates/spikard-http/src/query_parser.rs +0 -369
  63. data/vendor/crates/spikard-http/src/response.rs +0 -399
  64. data/vendor/crates/spikard-http/src/router.rs +0 -1
  65. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  66. data/vendor/crates/spikard-http/src/server/handler.rs +0 -80
  67. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -98
  68. data/vendor/crates/spikard-http/src/server/mod.rs +0 -805
  69. data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -119
  70. data/vendor/crates/spikard-http/src/sse.rs +0 -447
  71. data/vendor/crates/spikard-http/src/testing/form.rs +0 -14
  72. data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -60
  73. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -285
  74. data/vendor/crates/spikard-http/src/testing.rs +0 -377
  75. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  76. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  77. data/vendor/crates/spikard-http/src/websocket.rs +0 -324
  78. data/vendor/crates/spikard-rb/Cargo.toml +0 -42
  79. data/vendor/crates/spikard-rb/build.rs +0 -8
  80. data/vendor/crates/spikard-rb/src/background.rs +0 -63
  81. data/vendor/crates/spikard-rb/src/config.rs +0 -294
  82. data/vendor/crates/spikard-rb/src/conversion.rs +0 -392
  83. data/vendor/crates/spikard-rb/src/di.rs +0 -409
  84. data/vendor/crates/spikard-rb/src/handler.rs +0 -534
  85. data/vendor/crates/spikard-rb/src/lib.rs +0 -2020
  86. data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -267
  87. data/vendor/crates/spikard-rb/src/server.rs +0 -283
  88. data/vendor/crates/spikard-rb/src/sse.rs +0 -231
  89. data/vendor/crates/spikard-rb/src/test_client.rs +0 -404
  90. data/vendor/crates/spikard-rb/src/test_sse.rs +0 -143
  91. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  92. data/vendor/crates/spikard-rb/src/websocket.rs +0 -233
@@ -1,190 +0,0 @@
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(&param_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(&param_schema, "/users");
125
- assert!(result.is_ok());
126
-
127
- let params = result.unwrap();
128
- assert_eq!(params.len(), 3);
129
-
130
- for param in &params {
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(&param_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(&param_schema, "/users");
185
- assert!(result.is_err());
186
- if let Err(err) = result {
187
- assert!(err.contains("properties"));
188
- }
189
- }
190
- }
@@ -1,308 +0,0 @@
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
- }
@@ -1,195 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- pub use spikard_core::parameters::*;
@@ -1 +0,0 @@
1
- pub use spikard_core::problem::*;