1use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct OpenAPISpec {
29 pub openapi: String,
30 pub info: serde_json::Value,
31 pub paths: HashMap<String, serde_json::Value>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub components: Option<Components>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub tags: Vec<Tag>,
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
40 pub servers: Vec<Server>,
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub security: Vec<SecurityRequirement>,
44}
45
46impl OpenAPISpec {
47 pub fn to_json_string(&self) -> String {
49 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
50 }
51}
52
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
55pub struct Components {
56 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
58 pub schemas: HashMap<String, Schema>,
59 #[serde(
61 rename = "securitySchemes",
62 default,
63 skip_serializing_if = "HashMap::is_empty"
64 )]
65 pub security_schemes: HashMap<String, SecurityScheme>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct SecurityRequirement {
73 #[serde(flatten)]
74 pub requirements: HashMap<String, Vec<String>>,
75}
76
77impl SecurityRequirement {
78 pub fn new(scheme: &str) -> Self {
79 let mut requirements = HashMap::new();
80 requirements.insert(scheme.to_string(), vec![]);
81 Self { requirements }
82 }
83
84 pub fn with_scopes(mut self, scheme: &str, scopes: Vec<String>) -> Self {
85 self.requirements.insert(scheme.to_string(), scopes);
86 self
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct PathInfo {
97 pub method: String,
98 pub summary: String,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub description: Option<String>,
101 #[serde(default, skip_serializing_if = "Vec::is_empty")]
102 pub tags: Vec<String>,
103 #[serde(default, skip_serializing_if = "Vec::is_empty")]
104 pub parameters: Vec<serde_json::Value>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub request_body: Option<RequestBody>,
107 pub responses: HashMap<String, serde_json::Value>,
108 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub security: Vec<SecurityRequirement>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub operation_id: Option<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub deprecated: Option<bool>,
114}
115
116impl PathInfo {
117 pub fn new(method: &str, summary: &str) -> Self {
118 Self {
119 method: method.to_string(),
120 summary: summary.to_string(),
121 description: None,
122 tags: vec![],
123 parameters: vec![],
124 request_body: None,
125 responses: HashMap::new(),
126 security: vec![],
127 operation_id: None,
128 deprecated: None,
129 }
130 }
131
132 pub fn with_response(mut self, code: &str, desc: &str) -> Self {
133 self.responses
134 .insert(code.to_string(), serde_json::json!({ "description": desc }));
135 self
136 }
137
138 pub fn with_response_schema(mut self, code: &str, desc: &str, schema_ref: &str) -> Self {
140 self.responses.insert(
141 code.to_string(),
142 serde_json::json!({
143 "description": desc,
144 "content": {
145 "application/json": {
146 "schema": { "$ref": schema_ref }
147 }
148 }
149 }),
150 );
151 self
152 }
153
154 pub fn with_parameter(mut self, param: serde_json::Value) -> Self {
155 self.parameters.push(param);
156 self
157 }
158
159 pub fn with_path_param(self, name: &str, desc: &str, required: bool) -> Self {
161 self.with_parameter(serde_json::json!({
162 "name": name,
163 "in": "path",
164 "description": desc,
165 "required": required,
166 "schema": { "type": "string" }
167 }))
168 }
169
170 pub fn with_query_param(self, name: &str, desc: &str, required: bool) -> Self {
172 self.with_parameter(serde_json::json!({
173 "name": name,
174 "in": "query",
175 "description": desc,
176 "required": required,
177 "schema": { "type": "string" }
178 }))
179 }
180
181 pub fn with_tag(mut self, tag: &str) -> Self {
183 self.tags.push(tag.to_string());
184 self
185 }
186
187 pub fn with_request_body(mut self, body: RequestBody) -> Self {
189 self.request_body = Some(body);
190 self
191 }
192
193 pub fn with_request_body_ref(self, desc: &str, schema_ref: &str, required: bool) -> Self {
195 self.with_request_body(RequestBody::new(desc, schema_ref, required))
196 }
197
198 pub fn with_operation_id(mut self, id: &str) -> Self {
200 self.operation_id = Some(id.to_string());
201 self
202 }
203
204 pub fn with_security(mut self, req: SecurityRequirement) -> Self {
206 self.security.push(req);
207 self
208 }
209
210 pub fn deprecated(mut self) -> Self {
212 self.deprecated = Some(true);
213 self
214 }
215
216 pub fn with_description(mut self, desc: &str) -> Self {
218 self.description = Some(desc.to_string());
219 self
220 }
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct RequestBody {
226 pub description: String,
227 pub content: HashMap<String, MediaType>,
228 pub required: bool,
229}
230
231impl RequestBody {
232 pub fn new(desc: &str, schema_ref: &str, required: bool) -> Self {
233 let mut content = HashMap::new();
234 content.insert(
235 "application/json".to_string(),
236 MediaType::with_schema_ref(schema_ref),
237 );
238 Self {
239 description: desc.to_string(),
240 content,
241 required,
242 }
243 }
244
245 pub fn with_content(mut self, content_type: &str, media: MediaType) -> Self {
246 self.content.insert(content_type.to_string(), media);
247 self
248 }
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct MediaType {
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub schema: Option<Schema>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub example: Option<serde_json::Value>,
258 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
259 pub examples: HashMap<String, Example>,
260}
261
262impl MediaType {
263 pub fn with_schema_ref(schema_ref: &str) -> Self {
264 Self {
265 schema: Some(Schema::ref_to(schema_ref)),
266 example: None,
267 examples: HashMap::new(),
268 }
269 }
270
271 pub fn with_schema(mut self, schema: Schema) -> Self {
272 self.schema = Some(schema);
273 self
274 }
275
276 pub fn with_example(mut self, example: serde_json::Value) -> Self {
277 self.example = Some(example);
278 self
279 }
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct Example {
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub summary: Option<String>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub description: Option<String>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub value: Option<serde_json::Value>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub external_value: Option<String>,
293}
294
295impl Example {
296 pub fn new(value: serde_json::Value) -> Self {
297 Self {
298 summary: None,
299 description: None,
300 value: Some(value),
301 external_value: None,
302 }
303 }
304
305 pub fn with_summary(mut self, summary: &str) -> Self {
306 self.summary = Some(summary.to_string());
307 self
308 }
309
310 pub fn with_description(mut self, desc: &str) -> Self {
311 self.description = Some(desc.to_string());
312 self
313 }
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(untagged)]
328pub enum Schema {
329 Ref {
331 #[serde(rename = "$ref")]
332 ref_path: String,
333 },
334 Object(ObjectType),
336 Array(ArrayType),
338 Primitive(PrimitiveSchema),
340}
341
342impl Schema {
343 pub fn ref_to(name: &str) -> Self {
345 Self::Ref {
346 ref_path: format!("#/components/schemas/{}", name),
347 }
348 }
349
350 pub fn object(obj: ObjectType) -> Self {
352 Self::Object(obj)
353 }
354
355 pub fn array(arr: ArrayType) -> Self {
357 Self::Array(arr)
358 }
359
360 pub fn string() -> Self {
362 Self::Primitive(PrimitiveSchema::string())
363 }
364
365 pub fn integer() -> Self {
367 Self::Primitive(PrimitiveSchema::integer())
368 }
369
370 pub fn number() -> Self {
372 Self::Primitive(PrimitiveSchema::number())
373 }
374
375 pub fn boolean() -> Self {
377 Self::Primitive(PrimitiveSchema::boolean())
378 }
379
380 pub fn is_ref(&self) -> bool {
382 matches!(self, Schema::Ref { .. })
383 }
384
385 pub fn is_object(&self) -> bool {
387 matches!(self, Schema::Object(_))
388 }
389
390 pub fn is_array(&self) -> bool {
392 matches!(self, Schema::Array(_))
393 }
394
395 pub fn with_format(self, format: &str) -> Self {
397 match self {
398 Self::Primitive(p) => Self::Primitive(p.with_format(format)),
399 _ => self,
400 }
401 }
402
403 pub fn with_description(self, desc: &str) -> Self {
405 match self {
406 Self::Primitive(p) => Self::Primitive(p.with_description(desc)),
407 _ => self,
408 }
409 }
410
411 pub fn with_example(self, value: serde_json::Value) -> Self {
413 match self {
414 Self::Primitive(p) => Self::Primitive(p.with_example(value)),
415 _ => self,
416 }
417 }
418
419 pub fn with_default(self, value: serde_json::Value) -> Self {
421 match self {
422 Self::Primitive(p) => Self::Primitive(p.with_default(value)),
423 _ => self,
424 }
425 }
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct ObjectType {
431 #[serde(rename = "type")]
432 pub schema_type: String,
433 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
435 pub properties: HashMap<String, Schema>,
436 #[serde(default, skip_serializing_if = "Vec::is_empty")]
438 pub required: Vec<String>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub description: Option<String>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub additional_properties: Option<Box<Schema>>,
443}
444
445impl ObjectType {
446 pub fn new() -> Self {
447 Self {
448 schema_type: "object".to_string(),
449 properties: HashMap::new(),
450 required: vec![],
451 description: None,
452 additional_properties: None,
453 }
454 }
455
456 pub fn with_property(mut self, name: &str, schema: Schema) -> Self {
458 self.properties.insert(name.to_string(), schema);
459 self
460 }
461
462 pub fn with_required_property(mut self, name: &str, schema: Schema) -> Self {
464 self.properties.insert(name.to_string(), schema);
465 self.required.push(name.to_string());
466 self
467 }
468
469 pub fn with_description(mut self, desc: &str) -> Self {
471 self.description = Some(desc.to_string());
472 self
473 }
474
475 pub fn with_additional_properties(mut self, schema: Schema) -> Self {
477 self.additional_properties = Some(Box::new(schema));
478 self
479 }
480}
481
482impl Default for ObjectType {
483 fn default() -> Self {
484 Self::new()
485 }
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct ArrayType {
491 #[serde(rename = "type")]
492 pub schema_type: String,
493 pub items: Box<Schema>,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub min_items: Option<u32>,
497 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub max_items: Option<u32>,
499 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub unique_items: Option<bool>,
501 #[serde(default, skip_serializing_if = "Option::is_none")]
502 pub description: Option<String>,
503}
504
505impl ArrayType {
506 pub fn new(items: Schema) -> Self {
507 Self {
508 schema_type: "array".to_string(),
509 items: Box::new(items),
510 min_items: None,
511 max_items: None,
512 unique_items: None,
513 description: None,
514 }
515 }
516
517 pub fn with_min_items(mut self, min: u32) -> Self {
518 self.min_items = Some(min);
519 self
520 }
521
522 pub fn with_max_items(mut self, max: u32) -> Self {
523 self.max_items = Some(max);
524 self
525 }
526
527 pub fn unique_items(mut self) -> Self {
528 self.unique_items = Some(true);
529 self
530 }
531
532 pub fn with_description(mut self, desc: &str) -> Self {
533 self.description = Some(desc.to_string());
534 self
535 }
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
540pub struct PrimitiveSchema {
541 #[serde(rename = "type")]
542 pub schema_type: String,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub format: Option<String>,
545 #[serde(default, skip_serializing_if = "Option::is_none")]
546 pub description: Option<String>,
547 #[serde(default, skip_serializing_if = "Option::is_none")]
548 pub default: Option<serde_json::Value>,
549 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub example: Option<serde_json::Value>,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub enum_values: Option<Vec<serde_json::Value>>,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
554 pub minimum: Option<f64>,
555 #[serde(default, skip_serializing_if = "Option::is_none")]
556 pub maximum: Option<f64>,
557 #[serde(default, skip_serializing_if = "Option::is_none")]
558 pub min_length: Option<u32>,
559 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub max_length: Option<u32>,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
562 pub pattern: Option<String>,
563}
564
565impl PrimitiveSchema {
566 pub fn string() -> Self {
567 Self {
568 schema_type: "string".to_string(),
569 format: None,
570 description: None,
571 default: None,
572 example: None,
573 enum_values: None,
574 minimum: None,
575 maximum: None,
576 min_length: None,
577 max_length: None,
578 pattern: None,
579 }
580 }
581
582 pub fn integer() -> Self {
583 Self {
584 schema_type: "integer".to_string(),
585 format: Some("int64".to_string()),
586 description: None,
587 default: None,
588 example: None,
589 enum_values: None,
590 minimum: None,
591 maximum: None,
592 min_length: None,
593 max_length: None,
594 pattern: None,
595 }
596 }
597
598 pub fn number() -> Self {
599 Self {
600 schema_type: "number".to_string(),
601 format: Some("double".to_string()),
602 description: None,
603 default: None,
604 example: None,
605 enum_values: None,
606 minimum: None,
607 maximum: None,
608 min_length: None,
609 max_length: None,
610 pattern: None,
611 }
612 }
613
614 pub fn boolean() -> Self {
615 Self {
616 schema_type: "boolean".to_string(),
617 format: None,
618 description: None,
619 default: None,
620 example: None,
621 enum_values: None,
622 minimum: None,
623 maximum: None,
624 min_length: None,
625 max_length: None,
626 pattern: None,
627 }
628 }
629
630 pub fn with_format(mut self, format: &str) -> Self {
631 self.format = Some(format.to_string());
632 self
633 }
634
635 pub fn with_description(mut self, desc: &str) -> Self {
636 self.description = Some(desc.to_string());
637 self
638 }
639
640 pub fn with_default(mut self, value: serde_json::Value) -> Self {
641 self.default = Some(value);
642 self
643 }
644
645 pub fn with_example(mut self, value: serde_json::Value) -> Self {
646 self.example = Some(value);
647 self
648 }
649
650 pub fn with_enum(mut self, values: Vec<serde_json::Value>) -> Self {
651 self.enum_values = Some(values);
652 self
653 }
654
655 pub fn with_range(mut self, min: f64, max: f64) -> Self {
656 self.minimum = Some(min);
657 self.maximum = Some(max);
658 self
659 }
660
661 pub fn with_length_range(mut self, min: u32, max: u32) -> Self {
662 self.min_length = Some(min);
663 self.max_length = Some(max);
664 self
665 }
666
667 pub fn with_pattern(mut self, pattern: &str) -> Self {
668 self.pattern = Some(pattern.to_string());
669 self
670 }
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize)]
679#[serde(tag = "type")]
680pub enum SecurityScheme {
681 #[serde(rename = "http")]
686 Http {
687 scheme: String,
689 #[serde(
691 rename = "bearerFormat",
692 default,
693 skip_serializing_if = "Option::is_none"
694 )]
695 bearer_format: Option<String>,
696 #[serde(default, skip_serializing_if = "Option::is_none")]
697 description: Option<String>,
698 },
699 #[serde(rename = "apiKey")]
701 ApiKey {
702 name: String,
703 #[serde(rename = "in")]
704 location: ApiKeyLocation,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
706 description: Option<String>,
707 },
708 #[serde(rename = "oauth2")]
710 OAuth2 {
711 flows: Box<OAuth2Flows>,
712 #[serde(default, skip_serializing_if = "Option::is_none")]
713 description: Option<String>,
714 },
715}
716
717impl SecurityScheme {
718 pub fn basic() -> Self {
720 Self::Http {
721 scheme: "basic".to_string(),
722 bearer_format: None,
723 description: None,
724 }
725 }
726
727 pub fn bearer(jwt_format: bool) -> Self {
729 Self::Http {
730 scheme: "bearer".to_string(),
731 bearer_format: if jwt_format {
732 Some("JWT".to_string())
733 } else {
734 None
735 },
736 description: None,
737 }
738 }
739
740 pub fn api_key(name: &str, location: ApiKeyLocation) -> Self {
742 Self::ApiKey {
743 name: name.to_string(),
744 location,
745 description: None,
746 }
747 }
748
749 pub fn oauth2(flows: OAuth2Flows) -> Self {
751 Self::OAuth2 {
752 flows: Box::new(flows),
753 description: None,
754 }
755 }
756
757 pub fn is_basic(&self) -> bool {
759 matches!(self, Self::Http { scheme, .. } if scheme == "basic")
760 }
761
762 pub fn is_bearer(&self) -> bool {
764 matches!(self, Self::Http { scheme, .. } if scheme == "bearer")
765 }
766
767 pub fn with_description(self, desc: &str) -> Self {
769 match self {
770 Self::Http {
771 scheme,
772 bearer_format,
773 ..
774 } => Self::Http {
775 scheme,
776 bearer_format,
777 description: Some(desc.to_string()),
778 },
779 Self::ApiKey { name, location, .. } => Self::ApiKey {
780 name,
781 location,
782 description: Some(desc.to_string()),
783 },
784 Self::OAuth2 { flows, .. } => Self::OAuth2 {
785 flows,
786 description: Some(desc.to_string()),
787 },
788 }
789 }
790}
791
792#[derive(Debug, Clone, Serialize, Deserialize)]
794#[serde(rename_all = "lowercase")]
795pub enum ApiKeyLocation {
796 Query,
797 Header,
798 Cookie,
799}
800
801#[derive(Debug, Clone, Default, Serialize, Deserialize)]
803pub struct OAuth2Flows {
804 #[serde(default, skip_serializing_if = "Option::is_none")]
805 pub implicit: Option<ImplicitFlow>,
806 #[serde(default, skip_serializing_if = "Option::is_none")]
807 pub password: Option<PasswordFlow>,
808 #[serde(default, skip_serializing_if = "Option::is_none")]
809 pub client_credentials: Option<ClientCredentialsFlow>,
810 #[serde(default, skip_serializing_if = "Option::is_none")]
811 pub authorization_code: Option<AuthorizationCodeFlow>,
812}
813
814impl OAuth2Flows {
815 pub fn new() -> Self {
816 Self::default()
817 }
818
819 pub fn with_implicit(mut self, flow: ImplicitFlow) -> Self {
820 self.implicit = Some(flow);
821 self
822 }
823
824 pub fn with_password(mut self, flow: PasswordFlow) -> Self {
825 self.password = Some(flow);
826 self
827 }
828
829 pub fn with_client_credentials(mut self, flow: ClientCredentialsFlow) -> Self {
830 self.client_credentials = Some(flow);
831 self
832 }
833
834 pub fn with_authorization_code(mut self, flow: AuthorizationCodeFlow) -> Self {
835 self.authorization_code = Some(flow);
836 self
837 }
838}
839
840#[derive(Debug, Clone, Serialize, Deserialize)]
842pub struct ImplicitFlow {
843 #[serde(rename = "authorizationUrl")]
844 pub authorization_url: String,
845 #[serde(
846 rename = "refreshUrl",
847 default,
848 skip_serializing_if = "Option::is_none"
849 )]
850 pub refresh_url: Option<String>,
851 pub scopes: HashMap<String, String>,
852}
853
854#[derive(Debug, Clone, Serialize, Deserialize)]
856pub struct PasswordFlow {
857 #[serde(rename = "tokenUrl")]
858 pub token_url: String,
859 #[serde(
860 rename = "refreshUrl",
861 default,
862 skip_serializing_if = "Option::is_none"
863 )]
864 pub refresh_url: Option<String>,
865 pub scopes: HashMap<String, String>,
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize)]
870pub struct ClientCredentialsFlow {
871 #[serde(rename = "tokenUrl")]
872 pub token_url: String,
873 #[serde(
874 rename = "refreshUrl",
875 default,
876 skip_serializing_if = "Option::is_none"
877 )]
878 pub refresh_url: Option<String>,
879 pub scopes: HashMap<String, String>,
880}
881
882#[derive(Debug, Clone, Serialize, Deserialize)]
884pub struct AuthorizationCodeFlow {
885 #[serde(rename = "authorizationUrl")]
886 pub authorization_url: String,
887 #[serde(rename = "tokenUrl")]
888 pub token_url: String,
889 #[serde(
890 rename = "refreshUrl",
891 default,
892 skip_serializing_if = "Option::is_none"
893 )]
894 pub refresh_url: Option<String>,
895 pub scopes: HashMap<String, String>,
896}
897
898#[derive(Debug, Clone, Serialize, Deserialize)]
904pub struct Tag {
905 pub name: String,
906 #[serde(default, skip_serializing_if = "Option::is_none")]
907 pub description: Option<String>,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
909 pub external_docs: Option<ExternalDocs>,
910}
911
912impl Tag {
913 pub fn new(name: &str) -> Self {
914 Self {
915 name: name.to_string(),
916 description: None,
917 external_docs: None,
918 }
919 }
920
921 pub fn with_description(mut self, desc: &str) -> Self {
922 self.description = Some(desc.to_string());
923 self
924 }
925
926 pub fn with_external_docs(mut self, docs: ExternalDocs) -> Self {
927 self.external_docs = Some(docs);
928 self
929 }
930}
931
932#[derive(Debug, Clone, Serialize, Deserialize)]
934pub struct ExternalDocs {
935 pub url: String,
936 #[serde(default, skip_serializing_if = "Option::is_none")]
937 pub description: Option<String>,
938}
939
940impl ExternalDocs {
941 pub fn new(url: &str) -> Self {
942 Self {
943 url: url.to_string(),
944 description: None,
945 }
946 }
947
948 pub fn with_description(mut self, desc: &str) -> Self {
949 self.description = Some(desc.to_string());
950 self
951 }
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize)]
960pub struct Server {
961 pub url: String,
962 #[serde(default, skip_serializing_if = "Option::is_none")]
963 pub description: Option<String>,
964 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
965 pub variables: HashMap<String, ServerVariable>,
966}
967
968impl Server {
969 pub fn new(url: &str) -> Self {
970 Self {
971 url: url.to_string(),
972 description: None,
973 variables: HashMap::new(),
974 }
975 }
976
977 pub fn with_description(mut self, desc: &str) -> Self {
978 self.description = Some(desc.to_string());
979 self
980 }
981
982 pub fn with_variable(mut self, name: &str, var: ServerVariable) -> Self {
983 self.variables.insert(name.to_string(), var);
984 self
985 }
986}
987
988#[derive(Debug, Clone, Serialize, Deserialize)]
990pub struct ServerVariable {
991 #[serde(rename = "default")]
992 pub default_value: String,
993 #[serde(default, skip_serializing_if = "Option::is_none")]
994 pub description: Option<String>,
995 #[serde(default, skip_serializing_if = "Vec::is_empty")]
996 pub enum_values: Vec<String>,
997}
998
999impl ServerVariable {
1000 pub fn new(default: &str) -> Self {
1001 Self {
1002 default_value: default.to_string(),
1003 description: None,
1004 enum_values: vec![],
1005 }
1006 }
1007
1008 pub fn with_description(mut self, desc: &str) -> Self {
1009 self.description = Some(desc.to_string());
1010 self
1011 }
1012
1013 pub fn with_enum(mut self, values: Vec<String>) -> Self {
1014 self.enum_values = values;
1015 self
1016 }
1017}
1018
1019pub struct ExampleBuilder;
1025
1026impl ExampleBuilder {
1027 pub fn from_schema(schema: &Schema) -> serde_json::Value {
1034 match schema {
1035 Schema::Ref { .. } => serde_json::Value::Null,
1036 Schema::Object(obj) => Self::from_object(obj),
1037 Schema::Array(arr) => {
1038 let item = Self::from_schema(&arr.items);
1039 serde_json::Value::Array(vec![item])
1040 }
1041 Schema::Primitive(p) => Self::from_primitive(p),
1042 }
1043 }
1044
1045 pub fn from_object(obj: &ObjectType) -> serde_json::Value {
1047 let mut map = serde_json::Map::new();
1048 for (name, schema) in &obj.properties {
1049 map.insert(name.clone(), Self::from_schema(schema));
1050 }
1051 serde_json::Value::Object(map)
1052 }
1053
1054 pub fn from_primitive(p: &PrimitiveSchema) -> serde_json::Value {
1056 if let Some(example) = &p.example {
1058 return example.clone();
1059 }
1060 if let Some(default) = &p.default {
1062 return default.clone();
1063 }
1064 if let Some(enum_vals) = &p.enum_values {
1066 if let Some(first) = enum_vals.first() {
1067 return first.clone();
1068 }
1069 }
1070 match p.schema_type.as_str() {
1072 "string" => match p.format.as_deref() {
1073 Some("date") => serde_json::Value::String("2026-01-01".to_string()),
1074 Some("date-time") => serde_json::Value::String("2026-01-01T00:00:00Z".to_string()),
1075 Some("email") => serde_json::Value::String("user@example.com".to_string()),
1076 Some("uuid") => {
1077 serde_json::Value::String("550e8400-e29b-41d4-a716-446655440000".to_string())
1078 }
1079 Some("uri") => serde_json::Value::String("https://example.com".to_string()),
1080 _ => serde_json::Value::String("string".to_string()),
1081 },
1082 "integer" => {
1083 serde_json::Value::Number(serde_json::Number::from(p.minimum.unwrap_or(0.0) as i64))
1084 }
1085 "number" => serde_json::json!(p.minimum.unwrap_or(0.0)),
1086 "boolean" => serde_json::Value::Bool(false),
1087 _ => serde_json::Value::Null,
1088 }
1089 }
1090}
1091
1092pub struct OpenAPIGenerator {
1097 paths: Vec<(String, PathInfo)>,
1098 info: serde_json::Value,
1099 schemas: HashMap<String, Schema>,
1100 security_schemes: HashMap<String, SecurityScheme>,
1101 tags: Vec<Tag>,
1102 servers: Vec<Server>,
1103 security: Vec<SecurityRequirement>,
1104}
1105
1106impl OpenAPIGenerator {
1107 pub fn new() -> Self {
1108 Self {
1109 paths: vec![],
1110 info: serde_json::json!({
1111 "title": "API",
1112 "version": "1.0.0",
1113 "description": "Generated by sz-orm-swagger"
1114 }),
1115 schemas: HashMap::new(),
1116 security_schemes: HashMap::new(),
1117 tags: vec![],
1118 servers: vec![],
1119 security: vec![],
1120 }
1121 }
1122
1123 pub fn with_info(mut self, info: serde_json::Value) -> Self {
1124 self.info = info;
1125 self
1126 }
1127
1128 pub fn register_path(&mut self, path: &str, info: PathInfo) -> &mut Self {
1130 self.paths.push((path.to_string(), info));
1131 self
1132 }
1133
1134 pub fn register_schema(&mut self, name: &str, schema: Schema) -> &mut Self {
1136 self.schemas.insert(name.to_string(), schema);
1137 self
1138 }
1139
1140 pub fn register_security_scheme(&mut self, name: &str, scheme: SecurityScheme) -> &mut Self {
1142 self.security_schemes.insert(name.to_string(), scheme);
1143 self
1144 }
1145
1146 pub fn add_tag(&mut self, tag: Tag) -> &mut Self {
1148 self.tags.push(tag);
1149 self
1150 }
1151
1152 pub fn add_server(&mut self, server: Server) -> &mut Self {
1154 self.servers.push(server);
1155 self
1156 }
1157
1158 pub fn with_global_security(mut self, req: SecurityRequirement) -> Self {
1160 self.security.push(req);
1161 self
1162 }
1163
1164 pub fn generate(&self) -> OpenAPISpec {
1166 let mut paths: HashMap<String, serde_json::Value> = HashMap::new();
1167 for (path, info) in &self.paths {
1168 let method = info.method.to_lowercase();
1169 let entry = paths
1170 .entry(path.clone())
1171 .or_insert_with(|| serde_json::json!({}));
1172 let mut op = serde_json::json!({
1173 "summary": info.summary,
1174 "parameters": info.parameters,
1175 "responses": info.responses
1176 });
1177 if !info.tags.is_empty() {
1178 op["tags"] = serde_json::json!(info.tags);
1179 }
1180 if let Some(desc) = &info.description {
1181 op["description"] = serde_json::json!(desc);
1182 }
1183 if let Some(body) = &info.request_body {
1184 op["requestBody"] = serde_json::json!(body);
1185 }
1186 if !info.security.is_empty() {
1187 op["security"] = serde_json::json!(info.security);
1188 }
1189 if let Some(op_id) = &info.operation_id {
1190 op["operationId"] = serde_json::json!(op_id);
1191 }
1192 if let Some(deprecated) = info.deprecated {
1193 op["deprecated"] = serde_json::json!(deprecated);
1194 }
1195 entry[method] = op;
1196 }
1197
1198 let components = if self.schemas.is_empty() && self.security_schemes.is_empty() {
1199 None
1200 } else {
1201 Some(Components {
1202 schemas: self.schemas.clone(),
1203 security_schemes: self.security_schemes.clone(),
1204 })
1205 };
1206
1207 OpenAPISpec {
1208 openapi: "3.0.0".to_string(),
1209 paths,
1210 info: self.info.clone(),
1211 components,
1212 tags: self.tags.clone(),
1213 servers: self.servers.clone(),
1214 security: self.security.clone(),
1215 }
1216 }
1217}
1218
1219impl Default for OpenAPIGenerator {
1220 fn default() -> Self {
1221 Self::new()
1222 }
1223}
1224
1225pub struct SwaggerUi {
1230 mount_path: String,
1231 spec: Option<OpenAPISpec>,
1232}
1233
1234impl SwaggerUi {
1235 pub fn new(path: &str) -> Self {
1236 Self {
1237 mount_path: path.to_string(),
1238 spec: None,
1239 }
1240 }
1241
1242 pub fn with_spec(mut self, spec: OpenAPISpec) -> Self {
1243 self.spec = Some(spec);
1244 self
1245 }
1246
1247 pub fn mount(&self) -> String {
1248 format!("{}docs", self.mount_path)
1249 }
1250
1251 pub fn render_html(&self) -> String {
1253 let spec_json = match &self.spec {
1254 Some(s) => s.to_json_string(),
1255 None => serde_json::json!({
1256 "openapi": "3.0.0",
1257 "info": { "title": "API", "version": "1.0.0" },
1258 "paths": {}
1259 })
1260 .to_string(),
1261 };
1262 let mount = self.mount();
1263 format!(
1264 r#"<!DOCTYPE html>
1265<html lang="en">
1266<head>
1267 <meta charset="UTF-8">
1268 <title>Swagger UI</title>
1269 <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4.19.0/swagger-ui.css">
1270</head>
1271<body>
1272 <div id="swagger-ui"></div>
1273 <script src="https://unpkg.com/swagger-ui-dist@4.19.0/swagger-ui-bundle.js"></script>
1274 <script src="https://unpkg.com/swagger-ui-dist@4.19.0/swagger-ui-standalone-preset.js"></script>
1275 <script>
1276 const spec = {spec};
1277 window.onload = () => {{
1278 SwaggerUIBundle({{
1279 spec: spec,
1280 dom_id: '#swagger-ui',
1281 url: '{mount}/openapi.json',
1282 presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
1283 layout: 'StandaloneLayout'
1284 }});
1285 }};
1286 </script>
1287</body>
1288</html>"#,
1289 spec = spec_json,
1290 mount = mount
1291 )
1292 }
1293}
1294
1295pub fn model_to_openapi_schema<T: sz_orm_core::Model>() -> Schema {
1326 let mut obj = ObjectType::new().with_description(T::table_name());
1327 for (name, type_str) in T::fields() {
1328 obj = obj.with_property(name, cast_type_to_schema(type_str));
1329 }
1330 Schema::object(obj)
1331}
1332
1333fn cast_type_to_schema(type_str: &str) -> Schema {
1335 match type_str {
1336 "integer" => Schema::integer(),
1337 "float" | "double" => Schema::number(),
1338 "boolean" => Schema::boolean(),
1339 "datetime" => Schema::string().with_format("date-time"),
1340 "date" => Schema::string().with_format("date"),
1341 "time" => Schema::string().with_format("time"),
1342 "json" | "array" | "bytes" => Schema::string(),
1344 _ => Schema::string(),
1345 }
1346}
1347
1348#[cfg(test)]
1349mod tests {
1350 use super::*;
1351
1352 #[test]
1353 fn test_gen_empty_has_no_paths() {
1354 let s = OpenAPIGenerator::new().generate();
1355 assert!(s.paths.is_empty());
1356 assert_eq!(s.info["title"], "API");
1357 assert_eq!(s.openapi, "3.0.0");
1358 }
1359
1360 #[test]
1361 fn test_register_and_generate_single_path() {
1362 let mut g = OpenAPIGenerator::new();
1363 g.register_path(
1364 "/users",
1365 PathInfo::new("GET", "List users").with_response("200", "OK"),
1366 );
1367 let spec = g.generate();
1368 let users = spec.paths.get("/users").expect("/users should exist");
1369 let get = users.get("get").expect("GET method should exist");
1370 assert_eq!(get["summary"], "List users");
1371 assert!(get["responses"]["200"].is_object());
1372 }
1373
1374 #[test]
1375 fn test_register_multiple_methods_same_path() {
1376 let mut g = OpenAPIGenerator::new();
1377 g.register_path(
1378 "/users",
1379 PathInfo::new("GET", "List users").with_response("200", "OK"),
1380 );
1381 g.register_path(
1382 "/users",
1383 PathInfo::new("POST", "Create user").with_response("201", "Created"),
1384 );
1385 let spec = g.generate();
1386 assert_eq!(spec.paths.len(), 1, "only one /users path key");
1387 let users = spec.paths.get("/users").unwrap();
1388 assert!(users.get("get").is_some());
1389 assert!(users.get("post").is_some());
1390 assert_eq!(users["get"]["summary"], "List users");
1391 assert_eq!(users["post"]["summary"], "Create user");
1392 assert_eq!(users["post"]["responses"]["201"]["description"], "Created");
1393 }
1394
1395 #[test]
1396 fn test_register_multiple_paths() {
1397 let mut g = OpenAPIGenerator::new();
1398 g.register_path("/users", PathInfo::new("GET", "List users"));
1399 g.register_path("/orders", PathInfo::new("GET", "List orders"));
1400 g.register_path(
1401 "/items/{id}",
1402 PathInfo::new("GET", "Get item").with_response("404", "Not found"),
1403 );
1404 let spec = g.generate();
1405 assert_eq!(spec.paths.len(), 3);
1406 assert!(spec.paths.contains_key("/users"));
1407 assert!(spec.paths.contains_key("/orders"));
1408 assert!(spec.paths.contains_key("/items/{id}"));
1409 }
1410
1411 #[test]
1412 fn test_ui_mount() {
1413 let ui = SwaggerUi::new("/api");
1414 assert_eq!(ui.mount(), "/apidocs");
1415 }
1416
1417 #[test]
1418 fn test_ui_html_contains_cdn_and_bundle() {
1419 let ui = SwaggerUi::new("/api").with_spec(OpenAPIGenerator::new().generate());
1420 let html = ui.render_html();
1421 assert!(html.contains("swagger-ui-dist"));
1422 assert!(html.contains("swagger-ui.css"));
1423 assert!(html.contains("swagger-ui-bundle.js"));
1424 assert!(html.contains("SwaggerUIBundle"));
1425 assert!(html.contains("id=\"swagger-ui\""));
1426 assert!(html.contains("<!DOCTYPE html>"));
1427 }
1428
1429 #[test]
1430 fn test_ui_html_embeds_spec_content() {
1431 let mut g = OpenAPIGenerator::new();
1432 g.register_path(
1433 "/items",
1434 PathInfo::new("GET", "List items").with_response("200", "OK"),
1435 );
1436 let ui = SwaggerUi::new("/api").with_spec(g.generate());
1437 let html = ui.render_html();
1438 assert!(html.contains("/items"));
1439 assert!(html.contains("List items"));
1440 assert!(html.contains("\"get\""));
1441 }
1442
1443 #[test]
1444 fn test_ui_html_without_spec_uses_default_spec() {
1445 let ui = SwaggerUi::new("/api");
1446 let html = ui.render_html();
1447 assert!(html.contains("swagger-ui"));
1448 assert!(html.contains("\"openapi\""));
1449 }
1450
1451 #[test]
1452 fn test_spec_to_json_string_is_valid_json() {
1453 let mut g = OpenAPIGenerator::new();
1454 g.register_path("/users", PathInfo::new("GET", "List users"));
1455 let spec = g.generate();
1456 let json = spec.to_json_string();
1457 let parsed: serde_json::Value = serde_json::from_str(&json).expect("should parse");
1458 assert!(parsed["paths"]["/users"]["get"].is_object());
1459 assert_eq!(parsed["openapi"], "3.0.0");
1460 }
1461
1462 #[test]
1463 fn test_path_info_builder() {
1464 let p = PathInfo::new("PUT", "Update user")
1465 .with_response("200", "OK")
1466 .with_response("404", "Not found")
1467 .with_parameter(serde_json::json!({"name": "id", "in": "path"}));
1468 assert_eq!(p.method, "PUT");
1469 assert_eq!(p.responses.len(), 2);
1470 assert_eq!(p.parameters.len(), 1);
1471 }
1472
1473 #[test]
1476 fn test_schema_ref_to() {
1477 let s = Schema::ref_to("User");
1478 match &s {
1479 Schema::Ref { ref_path } => {
1480 assert_eq!(ref_path, "#/components/schemas/User");
1481 }
1482 _ => panic!("Expected Ref variant"),
1483 }
1484 assert!(s.is_ref());
1485 assert!(!s.is_object());
1486 assert!(!s.is_array());
1487 }
1488
1489 #[test]
1490 fn test_schema_primitive_string() {
1491 let s = Schema::string();
1492 match &s {
1493 Schema::Primitive(p) => {
1494 assert_eq!(p.schema_type, "string");
1495 }
1496 _ => panic!("Expected Primitive variant"),
1497 }
1498 }
1499
1500 #[test]
1501 fn test_schema_primitive_integer_with_format() {
1502 let s = Schema::integer();
1503 match &s {
1504 Schema::Primitive(p) => {
1505 assert_eq!(p.schema_type, "integer");
1506 assert_eq!(p.format.as_deref(), Some("int64"));
1507 }
1508 _ => panic!("Expected Primitive variant"),
1509 }
1510 }
1511
1512 #[test]
1513 fn test_schema_primitive_number() {
1514 let s = Schema::number();
1515 match &s {
1516 Schema::Primitive(p) => {
1517 assert_eq!(p.schema_type, "number");
1518 assert_eq!(p.format.as_deref(), Some("double"));
1519 }
1520 _ => panic!("Expected Primitive variant"),
1521 }
1522 }
1523
1524 #[test]
1525 fn test_schema_primitive_boolean() {
1526 let s = Schema::boolean();
1527 match &s {
1528 Schema::Primitive(p) => {
1529 assert_eq!(p.schema_type, "boolean");
1530 }
1531 _ => panic!("Expected Primitive variant"),
1532 }
1533 }
1534
1535 #[test]
1536 fn test_object_type_builder() {
1537 let obj = ObjectType::new()
1538 .with_required_property("id", Schema::integer())
1539 .with_property("name", Schema::string())
1540 .with_description("User object");
1541 assert_eq!(obj.schema_type, "object");
1542 assert_eq!(obj.properties.len(), 2);
1543 assert!(obj.required.contains(&"id".to_string()));
1544 assert!(!obj.required.contains(&"name".to_string()));
1545 assert_eq!(obj.description.as_deref(), Some("User object"));
1546 }
1547
1548 #[test]
1549 fn test_object_type_additional_properties() {
1550 let obj = ObjectType::new()
1551 .with_description("String map")
1552 .with_additional_properties(Schema::string());
1553 assert!(obj.additional_properties.is_some());
1554 }
1555
1556 #[test]
1557 fn test_array_type_builder() {
1558 let arr = ArrayType::new(Schema::string())
1559 .with_min_items(1)
1560 .with_max_items(100)
1561 .unique_items()
1562 .with_description("List of names");
1563 assert_eq!(arr.schema_type, "array");
1564 assert_eq!(arr.min_items, Some(1));
1565 assert_eq!(arr.max_items, Some(100));
1566 assert_eq!(arr.unique_items, Some(true));
1567 assert_eq!(arr.description.as_deref(), Some("List of names"));
1568 }
1569
1570 #[test]
1571 fn test_schema_is_object_array() {
1572 let obj_schema = Schema::object(ObjectType::new());
1573 assert!(obj_schema.is_object());
1574 assert!(!obj_schema.is_array());
1575
1576 let arr_schema = Schema::array(ArrayType::new(Schema::string()));
1577 assert!(!arr_schema.is_object());
1578 assert!(arr_schema.is_array());
1579 }
1580
1581 #[test]
1582 fn test_primitive_schema_with_format() {
1583 let p = PrimitiveSchema::string()
1584 .with_format("email")
1585 .with_description("Email address");
1586 assert_eq!(p.format.as_deref(), Some("email"));
1587 assert_eq!(p.description.as_deref(), Some("Email address"));
1588 }
1589
1590 #[test]
1591 fn test_primitive_schema_with_default() {
1592 let p = PrimitiveSchema::integer().with_default(serde_json::json!(42));
1593 assert_eq!(p.default, Some(serde_json::json!(42)));
1594 }
1595
1596 #[test]
1597 fn test_primitive_schema_with_example() {
1598 let p = PrimitiveSchema::string().with_example(serde_json::json!("John Doe"));
1599 assert_eq!(p.example, Some(serde_json::json!("John Doe")));
1600 }
1601
1602 #[test]
1603 fn test_primitive_schema_with_enum() {
1604 let p = PrimitiveSchema::string().with_enum(vec![
1605 serde_json::json!("active"),
1606 serde_json::json!("inactive"),
1607 ]);
1608 assert!(p.enum_values.is_some());
1609 assert_eq!(p.enum_values.as_ref().unwrap().len(), 2);
1610 }
1611
1612 #[test]
1613 fn test_primitive_schema_with_range() {
1614 let p = PrimitiveSchema::integer().with_range(0.0, 100.0);
1615 assert_eq!(p.minimum, Some(0.0));
1616 assert_eq!(p.maximum, Some(100.0));
1617 }
1618
1619 #[test]
1620 fn test_primitive_schema_with_length_range() {
1621 let p = PrimitiveSchema::string().with_length_range(3, 50);
1622 assert_eq!(p.min_length, Some(3));
1623 assert_eq!(p.max_length, Some(50));
1624 }
1625
1626 #[test]
1627 fn test_primitive_schema_with_pattern() {
1628 let p = PrimitiveSchema::string().with_pattern("^[a-z]+$");
1629 assert_eq!(p.pattern.as_deref(), Some("^[a-z]+$"));
1630 }
1631
1632 #[test]
1633 fn test_schema_serialization() {
1634 let obj = ObjectType::new()
1635 .with_required_property("id", Schema::integer())
1636 .with_property("name", Schema::string());
1637 let schema = Schema::object(obj);
1638 let json = serde_json::to_string(&schema).unwrap();
1639 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1640 assert_eq!(parsed["type"], "object");
1641 assert!(parsed["properties"]["id"].is_object());
1642 assert!(parsed["required"].is_array());
1643 }
1644
1645 #[test]
1648 fn test_security_scheme_basic() {
1649 let s = SecurityScheme::basic();
1650 assert!(s.is_basic());
1651 assert!(!s.is_bearer());
1652 match &s {
1653 SecurityScheme::Http { scheme, .. } => {
1654 assert_eq!(scheme, "basic");
1655 }
1656 _ => panic!("Expected Http variant"),
1657 }
1658 }
1659
1660 #[test]
1661 fn test_security_scheme_bearer_with_jwt() {
1662 let s = SecurityScheme::bearer(true);
1663 assert!(s.is_bearer());
1664 assert!(!s.is_basic());
1665 match &s {
1666 SecurityScheme::Http {
1667 scheme,
1668 bearer_format,
1669 ..
1670 } => {
1671 assert_eq!(scheme, "bearer");
1672 assert_eq!(bearer_format.as_deref(), Some("JWT"));
1673 }
1674 _ => panic!("Expected Http variant"),
1675 }
1676 }
1677
1678 #[test]
1679 fn test_security_scheme_bearer_without_format() {
1680 let s = SecurityScheme::bearer(false);
1681 match &s {
1682 SecurityScheme::Http { bearer_format, .. } => {
1683 assert!(bearer_format.is_none());
1684 }
1685 _ => panic!("Expected Http variant"),
1686 }
1687 }
1688
1689 #[test]
1690 fn test_security_scheme_api_key_header() {
1691 let s = SecurityScheme::api_key("X-API-Key", ApiKeyLocation::Header);
1692 match &s {
1693 SecurityScheme::ApiKey { name, location, .. } => {
1694 assert_eq!(name, "X-API-Key");
1695 assert!(matches!(location, ApiKeyLocation::Header));
1696 }
1697 _ => panic!("Expected ApiKey variant"),
1698 }
1699 }
1700
1701 #[test]
1702 fn test_security_scheme_api_key_query() {
1703 let s = SecurityScheme::api_key("api_key", ApiKeyLocation::Query);
1704 match &s {
1705 SecurityScheme::ApiKey { location, .. } => {
1706 assert!(matches!(location, ApiKeyLocation::Query));
1707 }
1708 _ => panic!("Expected ApiKey variant"),
1709 }
1710 }
1711
1712 #[test]
1713 fn test_security_scheme_api_key_cookie() {
1714 let s = SecurityScheme::api_key("session", ApiKeyLocation::Cookie);
1715 match &s {
1716 SecurityScheme::ApiKey { location, .. } => {
1717 assert!(matches!(location, ApiKeyLocation::Cookie));
1718 }
1719 _ => panic!("Expected ApiKey variant"),
1720 }
1721 }
1722
1723 #[test]
1724 fn test_security_scheme_oauth2() {
1725 let mut scopes = HashMap::new();
1726 scopes.insert("read".to_string(), "Read access".to_string());
1727 scopes.insert("write".to_string(), "Write access".to_string());
1728 let flow = AuthorizationCodeFlow {
1729 authorization_url: "https://example.com/oauth/authorize".to_string(),
1730 token_url: "https://example.com/oauth/token".to_string(),
1731 refresh_url: None,
1732 scopes,
1733 };
1734 let flows = OAuth2Flows::new().with_authorization_code(flow);
1735 let s = SecurityScheme::oauth2(flows);
1736 match &s {
1737 SecurityScheme::OAuth2 { flows, .. } => {
1738 assert!(flows.authorization_code.is_some());
1739 assert!(flows.implicit.is_none());
1740 }
1741 _ => panic!("Expected OAuth2 variant"),
1742 }
1743 }
1744
1745 #[test]
1746 fn test_security_scheme_with_description() {
1747 let s = SecurityScheme::bearer(true).with_description("JWT auth");
1748 match &s {
1749 SecurityScheme::Http { description, .. } => {
1750 assert_eq!(description.as_deref(), Some("JWT auth"));
1751 }
1752 _ => panic!("Expected Http variant"),
1753 }
1754 }
1755
1756 #[test]
1757 fn test_oauth2_flows_builder() {
1758 let mut scopes = HashMap::new();
1759 scopes.insert("read".to_string(), "Read access".to_string());
1760 let implicit = ImplicitFlow {
1761 authorization_url: "https://example.com/oauth/authorize".to_string(),
1762 refresh_url: None,
1763 scopes,
1764 };
1765 let flows = OAuth2Flows::new()
1766 .with_implicit(implicit)
1767 .with_password(PasswordFlow {
1768 token_url: "https://example.com/oauth/token".to_string(),
1769 refresh_url: None,
1770 scopes: HashMap::new(),
1771 })
1772 .with_client_credentials(ClientCredentialsFlow {
1773 token_url: "https://example.com/oauth/token".to_string(),
1774 refresh_url: None,
1775 scopes: HashMap::new(),
1776 });
1777 assert!(flows.implicit.is_some());
1778 assert!(flows.password.is_some());
1779 assert!(flows.client_credentials.is_some());
1780 assert!(flows.authorization_code.is_none());
1781 }
1782
1783 #[test]
1786 fn test_tag_builder() {
1787 let tag = Tag::new("users")
1788 .with_description("User management endpoints")
1789 .with_external_docs(
1790 ExternalDocs::new("https://example.com/docs/users").with_description("User docs"),
1791 );
1792 assert_eq!(tag.name, "users");
1793 assert_eq!(
1794 tag.description.as_deref(),
1795 Some("User management endpoints")
1796 );
1797 assert!(tag.external_docs.is_some());
1798 assert_eq!(
1799 tag.external_docs.as_ref().unwrap().url,
1800 "https://example.com/docs/users"
1801 );
1802 }
1803
1804 #[test]
1805 fn test_external_docs_builder() {
1806 let docs = ExternalDocs::new("https://example.com/docs")
1807 .with_description("External documentation");
1808 assert_eq!(docs.url, "https://example.com/docs");
1809 assert_eq!(docs.description.as_deref(), Some("External documentation"));
1810 }
1811
1812 #[test]
1815 fn test_server_builder() {
1816 let server = Server::new("https://{env}.example.com")
1817 .with_description("Environment-specific server")
1818 .with_variable(
1819 "env",
1820 ServerVariable::new("api")
1821 .with_description("Environment name")
1822 .with_enum(vec![
1823 "api".to_string(),
1824 "staging".to_string(),
1825 "prod".to_string(),
1826 ]),
1827 );
1828 assert_eq!(server.url, "https://{env}.example.com");
1829 assert_eq!(
1830 server.description.as_deref(),
1831 Some("Environment-specific server")
1832 );
1833 assert_eq!(server.variables.len(), 1);
1834 let env_var = server.variables.get("env").unwrap();
1835 assert_eq!(env_var.default_value, "api");
1836 assert_eq!(env_var.enum_values.len(), 3);
1837 }
1838
1839 #[test]
1840 fn test_server_variable_builder() {
1841 let var = ServerVariable::new("v1")
1842 .with_description("API version")
1843 .with_enum(vec!["v1".to_string(), "v2".to_string()]);
1844 assert_eq!(var.default_value, "v1");
1845 assert_eq!(var.description.as_deref(), Some("API version"));
1846 assert_eq!(var.enum_values, vec!["v1".to_string(), "v2".to_string()]);
1847 }
1848
1849 #[test]
1852 fn test_example_builder_primitive_string() {
1853 let p = PrimitiveSchema::string();
1854 let v = ExampleBuilder::from_primitive(&p);
1855 assert_eq!(v, serde_json::Value::String("string".to_string()));
1856 }
1857
1858 #[test]
1859 fn test_example_builder_primitive_string_with_example() {
1860 let p = PrimitiveSchema::string().with_example(serde_json::json!("hello"));
1861 let v = ExampleBuilder::from_primitive(&p);
1862 assert_eq!(v, serde_json::json!("hello"));
1863 }
1864
1865 #[test]
1866 fn test_example_builder_primitive_string_with_default() {
1867 let p = PrimitiveSchema::string().with_default(serde_json::json!("default_val"));
1868 let v = ExampleBuilder::from_primitive(&p);
1869 assert_eq!(v, serde_json::json!("default_val"));
1870 }
1871
1872 #[test]
1873 fn test_example_builder_primitive_email_format() {
1874 let p = PrimitiveSchema::string().with_format("email");
1875 let v = ExampleBuilder::from_primitive(&p);
1876 assert_eq!(v, serde_json::json!("user@example.com"));
1877 }
1878
1879 #[test]
1880 fn test_example_builder_primitive_date_format() {
1881 let p = PrimitiveSchema::string().with_format("date");
1882 let v = ExampleBuilder::from_primitive(&p);
1883 assert_eq!(v, serde_json::json!("2026-01-01"));
1884 }
1885
1886 #[test]
1887 fn test_example_builder_primitive_datetime_format() {
1888 let p = PrimitiveSchema::string().with_format("date-time");
1889 let v = ExampleBuilder::from_primitive(&p);
1890 assert_eq!(v, serde_json::json!("2026-01-01T00:00:00Z"));
1891 }
1892
1893 #[test]
1894 fn test_example_builder_primitive_uuid_format() {
1895 let p = PrimitiveSchema::string().with_format("uuid");
1896 let v = ExampleBuilder::from_primitive(&p);
1897 assert!(v.as_str().unwrap().contains("-"));
1898 }
1899
1900 #[test]
1901 fn test_example_builder_primitive_integer() {
1902 let p = PrimitiveSchema::integer();
1903 let v = ExampleBuilder::from_primitive(&p);
1904 assert_eq!(v, serde_json::json!(0));
1905 }
1906
1907 #[test]
1908 fn test_example_builder_primitive_integer_with_minimum() {
1909 let p = PrimitiveSchema::integer().with_range(10.0, 100.0);
1910 let v = ExampleBuilder::from_primitive(&p);
1911 assert_eq!(v, serde_json::json!(10));
1912 }
1913
1914 #[test]
1915 fn test_example_builder_primitive_number() {
1916 let p = PrimitiveSchema::number();
1917 let v = ExampleBuilder::from_primitive(&p);
1918 assert_eq!(v, serde_json::json!(0.0));
1919 }
1920
1921 #[test]
1922 fn test_example_builder_primitive_boolean() {
1923 let p = PrimitiveSchema::boolean();
1924 let v = ExampleBuilder::from_primitive(&p);
1925 assert_eq!(v, serde_json::json!(false));
1926 }
1927
1928 #[test]
1929 fn test_example_builder_primitive_with_enum() {
1930 let p = PrimitiveSchema::string().with_enum(vec![
1931 serde_json::json!("active"),
1932 serde_json::json!("inactive"),
1933 ]);
1934 let v = ExampleBuilder::from_primitive(&p);
1935 assert_eq!(v, serde_json::json!("active"));
1936 }
1937
1938 #[test]
1939 fn test_example_builder_object() {
1940 let obj = ObjectType::new()
1941 .with_required_property("id", Schema::integer())
1942 .with_property("name", Schema::string());
1943 let v = ExampleBuilder::from_object(&obj);
1944 assert!(v.is_object());
1945 assert_eq!(v["id"], serde_json::json!(0));
1946 assert_eq!(v["name"], serde_json::json!("string"));
1947 }
1948
1949 #[test]
1950 fn test_example_builder_array() {
1951 let arr = ArrayType::new(Schema::string());
1952 let schema = Schema::array(arr);
1953 let v = ExampleBuilder::from_schema(&schema);
1954 assert!(v.is_array());
1955 assert_eq!(v[0], serde_json::json!("string"));
1956 }
1957
1958 #[test]
1959 fn test_example_builder_ref_returns_null() {
1960 let schema = Schema::ref_to("User");
1961 let v = ExampleBuilder::from_schema(&schema);
1962 assert!(v.is_null());
1963 }
1964
1965 #[test]
1968 fn test_generator_with_schema() {
1969 let mut g = OpenAPIGenerator::new();
1970 let user_schema = Schema::object(
1971 ObjectType::new()
1972 .with_required_property("id", Schema::integer())
1973 .with_required_property("name", Schema::string())
1974 .with_property("email", Schema::string().with_format("email")),
1975 );
1976 g.register_schema("User", user_schema);
1977 let spec = g.generate();
1978 let components = spec.components.expect("components should exist");
1979 assert!(components.schemas.contains_key("User"));
1980 }
1981
1982 #[test]
1983 fn test_generator_with_security_scheme() {
1984 let mut g = OpenAPIGenerator::new();
1985 g.register_security_scheme("bearerAuth", SecurityScheme::bearer(true));
1986 let spec = g.generate();
1987 let components = spec.components.expect("components should exist");
1988 assert!(components.security_schemes.contains_key("bearerAuth"));
1989 }
1990
1991 #[test]
1992 fn test_generator_with_tags() {
1993 let mut g = OpenAPIGenerator::new();
1994 g.add_tag(Tag::new("users").with_description("User endpoints"));
1995 g.add_tag(Tag::new("orders").with_description("Order endpoints"));
1996 let spec = g.generate();
1997 assert_eq!(spec.tags.len(), 2);
1998 assert_eq!(spec.tags[0].name, "users");
1999 assert_eq!(spec.tags[1].name, "orders");
2000 }
2001
2002 #[test]
2003 fn test_generator_with_servers() {
2004 let mut g = OpenAPIGenerator::new();
2005 g.add_server(Server::new("https://api.example.com").with_description("Production"));
2006 g.add_server(Server::new("https://staging.example.com").with_description("Staging"));
2007 let spec = g.generate();
2008 assert_eq!(spec.servers.len(), 2);
2009 assert_eq!(spec.servers[0].url, "https://api.example.com");
2010 assert_eq!(spec.servers[1].url, "https://staging.example.com");
2011 }
2012
2013 #[test]
2014 fn test_generator_with_global_security() {
2015 let g =
2016 OpenAPIGenerator::new().with_global_security(SecurityRequirement::new("bearerAuth"));
2017 let spec = g.generate();
2018 assert_eq!(spec.security.len(), 1);
2019 }
2020
2021 #[test]
2022 fn test_path_info_with_request_body() {
2023 let p = PathInfo::new("POST", "Create user").with_request_body_ref(
2024 "User payload",
2025 "#/components/schemas/User",
2026 true,
2027 );
2028 assert!(p.request_body.is_some());
2029 let body = p.request_body.unwrap();
2030 assert!(body.required);
2031 assert!(body.content.contains_key("application/json"));
2032 }
2033
2034 #[test]
2035 fn test_path_info_with_tags_and_security() {
2036 let p = PathInfo::new("GET", "List users")
2037 .with_tag("users")
2038 .with_tag("admin")
2039 .with_security(SecurityRequirement::new("bearerAuth"));
2040 assert_eq!(p.tags, vec!["users", "admin"]);
2041 assert_eq!(p.security.len(), 1);
2042 }
2043
2044 #[test]
2045 fn test_path_info_with_path_param() {
2046 let p = PathInfo::new("GET", "Get user").with_path_param("id", "User ID", true);
2047 assert_eq!(p.parameters.len(), 1);
2048 assert_eq!(p.parameters[0]["in"], "path");
2049 assert_eq!(p.parameters[0]["name"], "id");
2050 assert_eq!(p.parameters[0]["required"], true);
2051 }
2052
2053 #[test]
2054 fn test_path_info_with_query_param() {
2055 let p = PathInfo::new("GET", "List users")
2056 .with_query_param("page", "Page number", false)
2057 .with_query_param("limit", "Items per page", false);
2058 assert_eq!(p.parameters.len(), 2);
2059 assert_eq!(p.parameters[0]["in"], "query");
2060 assert_eq!(p.parameters[1]["in"], "query");
2061 }
2062
2063 #[test]
2064 fn test_path_info_with_operation_id() {
2065 let p = PathInfo::new("GET", "Get user").with_operation_id("getUserById");
2066 assert_eq!(p.operation_id.as_deref(), Some("getUserById"));
2067 }
2068
2069 #[test]
2070 fn test_path_info_deprecated() {
2071 let p = PathInfo::new("GET", "Old endpoint").deprecated();
2072 assert_eq!(p.deprecated, Some(true));
2073 }
2074
2075 #[test]
2076 fn test_path_info_with_response_schema() {
2077 let p = PathInfo::new("GET", "Get user").with_response_schema(
2078 "200",
2079 "OK",
2080 "#/components/schemas/User",
2081 );
2082 let resp = &p.responses["200"];
2083 assert_eq!(resp["description"], "OK");
2084 assert_eq!(
2085 resp["content"]["application/json"]["schema"]["$ref"],
2086 "#/components/schemas/User"
2087 );
2088 }
2089
2090 #[test]
2091 fn test_security_requirement_with_scopes() {
2092 let req = SecurityRequirement::new("oauth2")
2093 .with_scopes("oauth2", vec!["read".to_string(), "write".to_string()]);
2094 assert_eq!(req.requirements.len(), 1);
2095 let scopes = req.requirements.get("oauth2").unwrap();
2096 assert_eq!(scopes.len(), 2);
2097 }
2098
2099 #[test]
2100 fn test_media_type_builder() {
2101 let media = MediaType::with_schema_ref("#/components/schemas/User")
2102 .with_example(serde_json::json!({"id": 1, "name": "John"}));
2103 assert!(media.schema.is_some());
2104 assert!(media.example.is_some());
2105 }
2106
2107 #[test]
2108 fn test_request_body_with_multiple_content_types() {
2109 let mut body = RequestBody::new("User data", "#/components/schemas/User", true);
2110 body = body.with_content(
2111 "application/xml",
2112 MediaType::with_schema_ref("#/components/schemas/User"),
2113 );
2114 assert_eq!(body.content.len(), 2);
2115 assert!(body.content.contains_key("application/json"));
2116 assert!(body.content.contains_key("application/xml"));
2117 }
2118
2119 #[test]
2120 fn test_example_builder() {
2121 let ex = Example::new(serde_json::json!({"id": 1, "name": "John"}))
2122 .with_summary("Sample user")
2123 .with_description("A typical user object");
2124 assert_eq!(ex.summary.as_deref(), Some("Sample user"));
2125 assert_eq!(ex.description.as_deref(), Some("A typical user object"));
2126 assert!(ex.value.is_some());
2127 }
2128
2129 #[test]
2130 fn test_full_spec_generation_with_all_components() {
2131 let mut g = OpenAPIGenerator::new();
2132
2133 let user_schema = Schema::object(
2135 ObjectType::new()
2136 .with_required_property("id", Schema::integer())
2137 .with_required_property("name", Schema::string())
2138 .with_property("email", Schema::string().with_format("email")),
2139 );
2140 g.register_schema("User", user_schema);
2141
2142 g.register_security_scheme("bearerAuth", SecurityScheme::bearer(true));
2144
2145 g.add_tag(Tag::new("users").with_description("User management"));
2147
2148 g.add_server(Server::new("https://api.example.com").with_description("Production"));
2150
2151 g.register_path(
2153 "/users",
2154 PathInfo::new("GET", "List users")
2155 .with_tag("users")
2156 .with_response_schema("200", "OK", "#/components/schemas/User")
2157 .with_security(SecurityRequirement::new("bearerAuth"))
2158 .with_operation_id("listUsers"),
2159 );
2160 g.register_path(
2161 "/users",
2162 PathInfo::new("POST", "Create user")
2163 .with_tag("users")
2164 .with_request_body_ref("User to create", "#/components/schemas/User", true)
2165 .with_response_schema("201", "Created", "#/components/schemas/User")
2166 .with_operation_id("createUser"),
2167 );
2168
2169 let spec = g.generate();
2170 let json = spec.to_json_string();
2171 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2172
2173 assert_eq!(parsed["openapi"], "3.0.0");
2174 assert!(parsed["components"]["schemas"]["User"].is_object());
2175 assert!(parsed["components"]["securitySchemes"]["bearerAuth"].is_object());
2176 assert_eq!(parsed["tags"][0]["name"], "users");
2177 assert_eq!(parsed["servers"][0]["url"], "https://api.example.com");
2178 assert!(parsed["paths"]["/users"]["get"].is_object());
2179 assert!(parsed["paths"]["/users"]["post"].is_object());
2180 assert_eq!(parsed["paths"]["/users"]["get"]["operationId"], "listUsers");
2181 assert_eq!(
2182 parsed["paths"]["/users"]["post"]["operationId"],
2183 "createUser"
2184 );
2185 }
2186
2187 #[test]
2188 fn test_model_to_openapi_schema() {
2189 use sz_orm_core::Model;
2190
2191 struct User;
2192 impl Model for User {
2193 type PrimaryKey = i64;
2194 fn table_name() -> &'static str {
2195 "users"
2196 }
2197 fn pk(&self) -> Self::PrimaryKey {
2198 0
2199 }
2200 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2201 fn fields() -> Vec<(&'static str, &'static str)> {
2202 vec![("id", "integer"), ("name", "string"), ("active", "boolean")]
2203 }
2204 }
2205
2206 let schema = model_to_openapi_schema::<User>();
2207 let json = serde_json::to_value(&schema).unwrap();
2208 assert_eq!(json["type"], "object");
2209 assert_eq!(json["description"], "users");
2210 assert_eq!(json["properties"]["id"]["type"], "integer");
2211 assert_eq!(json["properties"]["name"]["type"], "string");
2212 assert_eq!(json["properties"]["active"]["type"], "boolean");
2213 }
2214
2215 #[test]
2216 fn test_model_to_openapi_schema_empty_fields() {
2217 use sz_orm_core::Model;
2218
2219 struct Empty;
2220 impl Model for Empty {
2221 type PrimaryKey = i64;
2222 fn table_name() -> &'static str {
2223 "empty"
2224 }
2225 fn pk(&self) -> Self::PrimaryKey {
2226 0
2227 }
2228 fn set_pk(&mut self, _pk: Self::PrimaryKey) {}
2229 }
2230
2231 let schema = model_to_openapi_schema::<Empty>();
2232 let json = serde_json::to_value(&schema).unwrap();
2233 assert_eq!(json["type"], "object");
2234 assert_eq!(json["description"], "empty");
2235 assert!(
2237 json.get("properties").is_none() || json["properties"].as_object().unwrap().is_empty()
2238 );
2239 }
2240}
2241
2242#[cfg(feature = "openapi-reverse")]
2243pub mod reverse;