spikard 0.4.0-x86_64-linux

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 (138) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +1 -0
  3. data/README.md +659 -0
  4. data/ext/spikard_rb/Cargo.toml +17 -0
  5. data/ext/spikard_rb/extconf.rb +10 -0
  6. data/ext/spikard_rb/src/lib.rs +6 -0
  7. data/lib/spikard/app.rb +405 -0
  8. data/lib/spikard/background.rb +27 -0
  9. data/lib/spikard/config.rb +396 -0
  10. data/lib/spikard/converters.rb +13 -0
  11. data/lib/spikard/handler_wrapper.rb +113 -0
  12. data/lib/spikard/provide.rb +214 -0
  13. data/lib/spikard/response.rb +173 -0
  14. data/lib/spikard/schema.rb +243 -0
  15. data/lib/spikard/sse.rb +111 -0
  16. data/lib/spikard/streaming_response.rb +44 -0
  17. data/lib/spikard/testing.rb +221 -0
  18. data/lib/spikard/upload_file.rb +131 -0
  19. data/lib/spikard/version.rb +5 -0
  20. data/lib/spikard/websocket.rb +59 -0
  21. data/lib/spikard.rb +43 -0
  22. data/sig/spikard.rbs +366 -0
  23. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +5 -0
  24. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +2 -0
  25. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  26. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +139 -0
  27. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +561 -0
  28. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  29. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  30. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +403 -0
  31. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +274 -0
  32. data/vendor/crates/spikard-bindings-shared/src/lib.rs +25 -0
  33. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +298 -0
  34. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +637 -0
  35. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +309 -0
  36. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  37. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +355 -0
  38. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +502 -0
  39. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +389 -0
  40. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +413 -0
  41. data/vendor/crates/spikard-core/Cargo.toml +40 -0
  42. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -0
  43. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -0
  44. data/vendor/crates/spikard-core/src/debug.rs +63 -0
  45. data/vendor/crates/spikard-core/src/di/container.rs +726 -0
  46. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -0
  47. data/vendor/crates/spikard-core/src/di/error.rs +118 -0
  48. data/vendor/crates/spikard-core/src/di/factory.rs +538 -0
  49. data/vendor/crates/spikard-core/src/di/graph.rs +545 -0
  50. data/vendor/crates/spikard-core/src/di/mod.rs +192 -0
  51. data/vendor/crates/spikard-core/src/di/resolved.rs +411 -0
  52. data/vendor/crates/spikard-core/src/di/value.rs +283 -0
  53. data/vendor/crates/spikard-core/src/errors.rs +39 -0
  54. data/vendor/crates/spikard-core/src/http.rs +153 -0
  55. data/vendor/crates/spikard-core/src/lib.rs +29 -0
  56. data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
  57. data/vendor/crates/spikard-core/src/metadata.rs +397 -0
  58. data/vendor/crates/spikard-core/src/parameters.rs +723 -0
  59. data/vendor/crates/spikard-core/src/problem.rs +310 -0
  60. data/vendor/crates/spikard-core/src/request_data.rs +189 -0
  61. data/vendor/crates/spikard-core/src/router.rs +249 -0
  62. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -0
  63. data/vendor/crates/spikard-core/src/type_hints.rs +304 -0
  64. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +689 -0
  65. data/vendor/crates/spikard-core/src/validation/mod.rs +459 -0
  66. data/vendor/crates/spikard-http/Cargo.toml +58 -0
  67. data/vendor/crates/spikard-http/examples/sse-notifications.rs +147 -0
  68. data/vendor/crates/spikard-http/examples/websocket-chat.rs +91 -0
  69. data/vendor/crates/spikard-http/src/auth.rs +247 -0
  70. data/vendor/crates/spikard-http/src/background.rs +1562 -0
  71. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -0
  72. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -0
  73. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -0
  74. data/vendor/crates/spikard-http/src/cors.rs +490 -0
  75. data/vendor/crates/spikard-http/src/debug.rs +63 -0
  76. data/vendor/crates/spikard-http/src/di_handler.rs +1878 -0
  77. data/vendor/crates/spikard-http/src/handler_response.rs +532 -0
  78. data/vendor/crates/spikard-http/src/handler_trait.rs +861 -0
  79. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
  80. data/vendor/crates/spikard-http/src/lib.rs +524 -0
  81. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -0
  82. data/vendor/crates/spikard-http/src/lifecycle.rs +428 -0
  83. data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -0
  84. data/vendor/crates/spikard-http/src/middleware/multipart.rs +930 -0
  85. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +541 -0
  86. data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -0
  87. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -0
  88. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -0
  89. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +867 -0
  90. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +678 -0
  91. data/vendor/crates/spikard-http/src/query_parser.rs +369 -0
  92. data/vendor/crates/spikard-http/src/response.rs +399 -0
  93. data/vendor/crates/spikard-http/src/server/handler.rs +1557 -0
  94. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
  95. data/vendor/crates/spikard-http/src/server/mod.rs +806 -0
  96. data/vendor/crates/spikard-http/src/server/request_extraction.rs +630 -0
  97. data/vendor/crates/spikard-http/src/server/routing_factory.rs +497 -0
  98. data/vendor/crates/spikard-http/src/sse.rs +961 -0
  99. data/vendor/crates/spikard-http/src/testing/form.rs +14 -0
  100. data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -0
  101. data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -0
  102. data/vendor/crates/spikard-http/src/testing.rs +377 -0
  103. data/vendor/crates/spikard-http/src/websocket.rs +831 -0
  104. data/vendor/crates/spikard-http/tests/background_behavior.rs +918 -0
  105. data/vendor/crates/spikard-http/tests/common/handlers.rs +308 -0
  106. data/vendor/crates/spikard-http/tests/common/mod.rs +21 -0
  107. data/vendor/crates/spikard-http/tests/di_integration.rs +202 -0
  108. data/vendor/crates/spikard-http/tests/doc_snippets.rs +4 -0
  109. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1135 -0
  110. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +688 -0
  111. data/vendor/crates/spikard-http/tests/server_config_builder.rs +324 -0
  112. data/vendor/crates/spikard-http/tests/sse_behavior.rs +728 -0
  113. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +724 -0
  114. data/vendor/crates/spikard-rb/Cargo.toml +43 -0
  115. data/vendor/crates/spikard-rb/build.rs +199 -0
  116. data/vendor/crates/spikard-rb/src/background.rs +63 -0
  117. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  118. data/vendor/crates/spikard-rb/src/config/server_config.rs +283 -0
  119. data/vendor/crates/spikard-rb/src/conversion.rs +459 -0
  120. data/vendor/crates/spikard-rb/src/di/builder.rs +105 -0
  121. data/vendor/crates/spikard-rb/src/di/mod.rs +413 -0
  122. data/vendor/crates/spikard-rb/src/handler.rs +612 -0
  123. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  124. data/vendor/crates/spikard-rb/src/lib.rs +1857 -0
  125. data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -0
  126. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  127. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +427 -0
  128. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  129. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +326 -0
  130. data/vendor/crates/spikard-rb/src/server.rs +283 -0
  131. data/vendor/crates/spikard-rb/src/sse.rs +231 -0
  132. data/vendor/crates/spikard-rb/src/testing/client.rs +404 -0
  133. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  134. data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -0
  135. data/vendor/crates/spikard-rb/src/testing/websocket.rs +221 -0
  136. data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
  137. data/vendor/crates/spikard-rb/tests/magnus_ffi_tests.rs +14 -0
  138. metadata +213 -0
@@ -0,0 +1,535 @@
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
+
191
+ #[test]
192
+ fn test_extract_parameters_with_format_specifiers() {
193
+ let param_schema = json!({
194
+ "type": "object",
195
+ "properties": {
196
+ "user_id": { "type": "string", "format": "uuid" },
197
+ "created_at": { "type": "string", "format": "date-time" },
198
+ "birth_date": { "type": "string", "format": "date" },
199
+ "email": { "type": "string", "format": "email" },
200
+ "website": { "type": "string", "format": "uri" }
201
+ },
202
+ "required": ["user_id"]
203
+ });
204
+
205
+ let result = extract_parameters_from_schema(&param_schema, "/users");
206
+ assert!(result.is_ok());
207
+
208
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
209
+ assert_eq!(params.len(), 5);
210
+
211
+ for param in params {
212
+ if let RefOr::T(p) = param {
213
+ assert!(matches!(p.parameter_in, ParameterIn::Query));
214
+ if p.name == "user_id" {
215
+ assert!(matches!(p.required, utoipa::openapi::Required::True));
216
+ } else {
217
+ assert!(matches!(p.required, utoipa::openapi::Required::False));
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ #[test]
224
+ fn test_extract_parameters_with_nullable_optional() {
225
+ let param_schema = json!({
226
+ "type": "object",
227
+ "properties": {
228
+ "search": { "type": "string" },
229
+ "filter": { "type": "string" }
230
+ },
231
+ "required": []
232
+ });
233
+
234
+ let result = extract_parameters_from_schema(&param_schema, "/items");
235
+ assert!(result.is_ok());
236
+
237
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
238
+ assert_eq!(params.len(), 2);
239
+
240
+ for param in params {
241
+ if let RefOr::T(p) = param {
242
+ assert!(matches!(p.required, utoipa::openapi::Required::False));
243
+ }
244
+ }
245
+ }
246
+
247
+ #[test]
248
+ fn test_extract_parameters_array_parameter() {
249
+ let param_schema = json!({
250
+ "type": "object",
251
+ "properties": {
252
+ "tags": {
253
+ "type": "array",
254
+ "items": { "type": "string" }
255
+ },
256
+ "ids": {
257
+ "type": "array",
258
+ "items": { "type": "integer" }
259
+ }
260
+ },
261
+ "required": ["tags"]
262
+ });
263
+
264
+ let result = extract_parameters_from_schema(&param_schema, "/search");
265
+ assert!(result.is_ok());
266
+
267
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
268
+ assert_eq!(params.len(), 2);
269
+
270
+ for param in params {
271
+ if let RefOr::T(p) = param {
272
+ if p.name == "tags" {
273
+ assert!(matches!(p.required, utoipa::openapi::Required::True));
274
+ } else if p.name == "ids" {
275
+ assert!(matches!(p.required, utoipa::openapi::Required::False));
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ #[test]
282
+ fn test_extract_parameters_empty_properties() {
283
+ let param_schema = json!({
284
+ "type": "object",
285
+ "properties": {},
286
+ "required": []
287
+ });
288
+
289
+ let result = extract_parameters_from_schema(&param_schema, "/items");
290
+ assert!(result.is_ok());
291
+
292
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
293
+ assert_eq!(params.len(), 0);
294
+ }
295
+
296
+ #[test]
297
+ fn test_extract_parameters_with_multiple_path_params() {
298
+ let param_schema = json!({
299
+ "type": "object",
300
+ "properties": {
301
+ "org_id": { "type": "string" },
302
+ "team_id": { "type": "string" },
303
+ "member_id": { "type": "string" },
304
+ "page": { "type": "integer" }
305
+ },
306
+ "required": ["org_id", "team_id", "member_id"]
307
+ });
308
+
309
+ let result =
310
+ extract_parameters_from_schema(&param_schema, "/orgs/{org_id}/teams/{team_id}/members/{member_id}");
311
+ assert!(result.is_ok());
312
+
313
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
314
+ assert_eq!(params.len(), 4);
315
+
316
+ let mut path_count: i32 = 0;
317
+ let mut query_count: i32 = 0;
318
+
319
+ for param in params {
320
+ if let RefOr::T(p) = param {
321
+ if matches!(p.parameter_in, ParameterIn::Path) {
322
+ path_count += 1;
323
+ assert!(matches!(p.required, utoipa::openapi::Required::True));
324
+ } else {
325
+ query_count += 1;
326
+ }
327
+ }
328
+ }
329
+
330
+ assert_eq!(path_count, 3);
331
+ assert_eq!(query_count, 1);
332
+ }
333
+
334
+ #[test]
335
+ fn test_extract_parameters_with_numeric_types() {
336
+ let param_schema = json!({
337
+ "type": "object",
338
+ "properties": {
339
+ "count": { "type": "integer" },
340
+ "score": { "type": "number" },
341
+ "active": { "type": "boolean" }
342
+ },
343
+ "required": ["count"]
344
+ });
345
+
346
+ let result = extract_parameters_from_schema(&param_schema, "/stats");
347
+ assert!(result.is_ok());
348
+
349
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
350
+ assert_eq!(params.len(), 3);
351
+
352
+ for param in params {
353
+ if let RefOr::T(p) = param {
354
+ assert!(matches!(p.parameter_in, ParameterIn::Query));
355
+ if p.name == "count" {
356
+ assert!(matches!(p.required, utoipa::openapi::Required::True));
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ #[test]
363
+ fn test_extract_parameters_required_field_parsing() {
364
+ let param_schema = json!({
365
+ "type": "object",
366
+ "properties": {
367
+ "id": { "type": "integer" },
368
+ "name": { "type": "string" },
369
+ "email": { "type": "string" },
370
+ "age": { "type": "integer" }
371
+ },
372
+ "required": ["id", "name"]
373
+ });
374
+
375
+ let result = extract_parameters_from_schema(&param_schema, "/items");
376
+ assert!(result.is_ok());
377
+
378
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
379
+ assert_eq!(params.len(), 4);
380
+
381
+ let required_names: Vec<&str> = vec!["id", "name"];
382
+
383
+ for param in params {
384
+ if let RefOr::T(p) = param {
385
+ if required_names.contains(&p.name.as_str()) {
386
+ assert!(matches!(p.required, utoipa::openapi::Required::True));
387
+ } else {
388
+ assert!(matches!(p.required, utoipa::openapi::Required::False));
389
+ }
390
+ }
391
+ }
392
+ }
393
+
394
+ #[test]
395
+ fn test_extract_parameters_single_path_param_override_required() {
396
+ let param_schema = json!({
397
+ "type": "object",
398
+ "properties": {
399
+ "id": { "type": "integer" },
400
+ "query": { "type": "string" }
401
+ },
402
+ "required": ["query"]
403
+ });
404
+
405
+ let result = extract_parameters_from_schema(&param_schema, "/items/{id}");
406
+ assert!(result.is_ok());
407
+
408
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
409
+ assert_eq!(params.len(), 2);
410
+
411
+ for param in params {
412
+ if let RefOr::T(p) = param {
413
+ if p.name == "id" {
414
+ assert!(matches!(p.parameter_in, ParameterIn::Path));
415
+ assert!(matches!(p.required, utoipa::openapi::Required::True));
416
+ } else if p.name == "query" {
417
+ assert!(matches!(p.parameter_in, ParameterIn::Query));
418
+ assert!(matches!(p.required, utoipa::openapi::Required::True));
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ #[test]
425
+ fn test_extract_parameters_nested_object_schema() {
426
+ let param_schema = json!({
427
+ "type": "object",
428
+ "properties": {
429
+ "filter": {
430
+ "type": "object",
431
+ "properties": {
432
+ "status": { "type": "string" },
433
+ "priority": { "type": "integer" }
434
+ },
435
+ "required": ["status"]
436
+ }
437
+ },
438
+ "required": ["filter"]
439
+ });
440
+
441
+ let result = extract_parameters_from_schema(&param_schema, "/tasks");
442
+ assert!(result.is_ok());
443
+
444
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
445
+ assert_eq!(params.len(), 1);
446
+
447
+ if let Some(RefOr::T(p)) = params.first() {
448
+ assert_eq!(p.name, "filter");
449
+ assert!(matches!(p.parameter_in, ParameterIn::Query));
450
+ assert!(matches!(p.required, utoipa::openapi::Required::True));
451
+ }
452
+ }
453
+
454
+ #[test]
455
+ fn test_extract_parameters_with_special_characters_in_names() {
456
+ let param_schema = json!({
457
+ "type": "object",
458
+ "properties": {
459
+ "user_id": { "type": "string" },
460
+ "api_key": { "type": "string" },
461
+ "x_custom_header": { "type": "string" }
462
+ },
463
+ "required": ["user_id"]
464
+ });
465
+
466
+ let result = extract_parameters_from_schema(&param_schema, "/data");
467
+ assert!(result.is_ok());
468
+
469
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
470
+ assert_eq!(params.len(), 3);
471
+
472
+ let param_names: Vec<String> = params
473
+ .iter()
474
+ .filter_map(|p| match p {
475
+ RefOr::T(param) => Some(param.name.clone()),
476
+ RefOr::Ref(_) => None,
477
+ })
478
+ .collect();
479
+
480
+ assert!(param_names.contains(&"user_id".to_string()));
481
+ assert!(param_names.contains(&"api_key".to_string()));
482
+ assert!(param_names.contains(&"x_custom_header".to_string()));
483
+ }
484
+
485
+ #[test]
486
+ fn test_extract_parameters_with_mismatched_required_field() {
487
+ let param_schema = json!({
488
+ "type": "object",
489
+ "properties": {
490
+ "id": { "type": "integer" },
491
+ "name": { "type": "string" }
492
+ },
493
+ "required": ["id", "nonexistent_field"]
494
+ });
495
+
496
+ let result = extract_parameters_from_schema(&param_schema, "/items");
497
+ assert!(result.is_ok());
498
+
499
+ let params: Vec<RefOr<Parameter>> = result.unwrap();
500
+ assert_eq!(params.len(), 2);
501
+
502
+ for param in params {
503
+ if let RefOr::T(p) = param {
504
+ if p.name == "id" {
505
+ assert!(matches!(p.required, utoipa::openapi::Required::True));
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ #[test]
512
+ fn test_extract_path_param_names_with_special_segments() {
513
+ let names: Vec<&str> =
514
+ extract_path_param_names("/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}");
515
+ assert_eq!(names, vec!["user_id", "post_id", "comment_id"]);
516
+ }
517
+
518
+ #[test]
519
+ fn test_extract_path_param_names_no_params() {
520
+ let names: Vec<&str> = extract_path_param_names("/api/users/list");
521
+ assert!(names.is_empty());
522
+ }
523
+
524
+ #[test]
525
+ fn test_extract_path_param_names_single_param_end() {
526
+ let names: Vec<&str> = extract_path_param_names("/resource/{id}");
527
+ assert_eq!(names, vec!["id"]);
528
+ }
529
+
530
+ #[test]
531
+ fn test_extract_path_param_names_numeric_param_names() {
532
+ let names: Vec<&str> = extract_path_param_names("/items/{id1}/sub/{id2}");
533
+ assert_eq!(names, vec!["id1", "id2"]);
534
+ }
535
+ }