spikard 0.2.1 → 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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +626 -626
  4. data/ext/spikard_rb/Cargo.toml +17 -17
  5. data/ext/spikard_rb/extconf.rb +10 -10
  6. data/ext/spikard_rb/src/lib.rs +6 -6
  7. data/lib/spikard/app.rb +374 -374
  8. data/lib/spikard/background.rb +27 -27
  9. data/lib/spikard/config.rb +396 -396
  10. data/lib/spikard/converters.rb +85 -85
  11. data/lib/spikard/handler_wrapper.rb +116 -116
  12. data/lib/spikard/provide.rb +228 -228
  13. data/lib/spikard/response.rb +109 -109
  14. data/lib/spikard/schema.rb +243 -243
  15. data/lib/spikard/sse.rb +111 -111
  16. data/lib/spikard/streaming_response.rb +21 -21
  17. data/lib/spikard/testing.rb +221 -221
  18. data/lib/spikard/upload_file.rb +131 -131
  19. data/lib/spikard/version.rb +5 -5
  20. data/lib/spikard/websocket.rb +59 -59
  21. data/lib/spikard.rb +43 -43
  22. data/sig/spikard.rbs +349 -349
  23. data/vendor/crates/spikard-core/Cargo.toml +40 -0
  24. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -0
  25. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -0
  26. data/vendor/crates/spikard-core/src/debug.rs +63 -0
  27. data/vendor/crates/spikard-core/src/di/container.rs +726 -0
  28. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -0
  29. data/vendor/crates/spikard-core/src/di/error.rs +118 -0
  30. data/vendor/crates/spikard-core/src/di/factory.rs +538 -0
  31. data/vendor/crates/spikard-core/src/di/graph.rs +545 -0
  32. data/vendor/crates/spikard-core/src/di/mod.rs +192 -0
  33. data/vendor/crates/spikard-core/src/di/resolved.rs +411 -0
  34. data/vendor/crates/spikard-core/src/di/value.rs +283 -0
  35. data/vendor/crates/spikard-core/src/http.rs +153 -0
  36. data/vendor/crates/spikard-core/src/lib.rs +28 -0
  37. data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
  38. data/vendor/crates/spikard-core/src/parameters.rs +719 -0
  39. data/vendor/crates/spikard-core/src/problem.rs +310 -0
  40. data/vendor/crates/spikard-core/src/request_data.rs +189 -0
  41. data/vendor/crates/spikard-core/src/router.rs +249 -0
  42. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -0
  43. data/vendor/crates/spikard-core/src/type_hints.rs +304 -0
  44. data/vendor/crates/spikard-core/src/validation.rs +699 -0
  45. data/vendor/crates/spikard-http/Cargo.toml +58 -0
  46. data/vendor/crates/spikard-http/src/auth.rs +247 -0
  47. data/vendor/crates/spikard-http/src/background.rs +249 -0
  48. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -0
  49. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -0
  50. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -0
  51. data/vendor/crates/spikard-http/src/cors.rs +490 -0
  52. data/vendor/crates/spikard-http/src/debug.rs +63 -0
  53. data/vendor/crates/spikard-http/src/di_handler.rs +423 -0
  54. data/vendor/crates/spikard-http/src/handler_response.rs +190 -0
  55. data/vendor/crates/spikard-http/src/handler_trait.rs +228 -0
  56. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
  57. data/vendor/crates/spikard-http/src/lib.rs +529 -0
  58. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -0
  59. data/vendor/crates/spikard-http/src/lifecycle.rs +428 -0
  60. data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -0
  61. data/vendor/crates/spikard-http/src/middleware/multipart.rs +86 -0
  62. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +147 -0
  63. data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -0
  64. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -0
  65. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +190 -0
  66. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +308 -0
  67. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +195 -0
  68. data/vendor/crates/spikard-http/src/parameters.rs +1 -0
  69. data/vendor/crates/spikard-http/src/problem.rs +1 -0
  70. data/vendor/crates/spikard-http/src/query_parser.rs +369 -0
  71. data/vendor/crates/spikard-http/src/response.rs +399 -0
  72. data/vendor/crates/spikard-http/src/router.rs +1 -0
  73. data/vendor/crates/spikard-http/src/schema_registry.rs +1 -0
  74. data/vendor/crates/spikard-http/src/server/handler.rs +80 -0
  75. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
  76. data/vendor/crates/spikard-http/src/server/mod.rs +805 -0
  77. data/vendor/crates/spikard-http/src/server/request_extraction.rs +119 -0
  78. data/vendor/crates/spikard-http/src/sse.rs +447 -0
  79. data/vendor/crates/spikard-http/src/testing/form.rs +14 -0
  80. data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -0
  81. data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -0
  82. data/vendor/crates/spikard-http/src/testing.rs +377 -0
  83. data/vendor/crates/spikard-http/src/type_hints.rs +1 -0
  84. data/vendor/crates/spikard-http/src/validation.rs +1 -0
  85. data/vendor/crates/spikard-http/src/websocket.rs +324 -0
  86. data/vendor/crates/spikard-rb/Cargo.toml +42 -0
  87. data/vendor/crates/spikard-rb/build.rs +8 -0
  88. data/vendor/crates/spikard-rb/src/background.rs +63 -0
  89. data/vendor/crates/spikard-rb/src/config.rs +294 -0
  90. data/vendor/crates/spikard-rb/src/conversion.rs +392 -0
  91. data/vendor/crates/spikard-rb/src/di.rs +409 -0
  92. data/vendor/crates/spikard-rb/src/handler.rs +534 -0
  93. data/vendor/crates/spikard-rb/src/lib.rs +2020 -0
  94. data/vendor/crates/spikard-rb/src/lifecycle.rs +267 -0
  95. data/vendor/crates/spikard-rb/src/server.rs +283 -0
  96. data/vendor/crates/spikard-rb/src/sse.rs +231 -0
  97. data/vendor/crates/spikard-rb/src/test_client.rs +404 -0
  98. data/vendor/crates/spikard-rb/src/test_sse.rs +143 -0
  99. data/vendor/crates/spikard-rb/src/test_websocket.rs +221 -0
  100. data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
  101. metadata +80 -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(&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
+ }
@@ -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::*;