domainstack_schema/
openapi.rs1use crate::{Schema, ToSchema};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct OpenApiSpec {
10 pub openapi: String,
11 pub info: Info,
12 pub components: Components,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Info {
18 pub title: String,
19 pub version: String,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub description: Option<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Components {
27 pub schemas: HashMap<String, Schema>,
28}
29
30pub struct OpenApiBuilder {
32 title: String,
33 version: String,
34 description: Option<String>,
35 schemas: HashMap<String, Schema>,
36}
37
38impl OpenApiBuilder {
39 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
51 Self {
52 title: title.into(),
53 version: version.into(),
54 description: None,
55 schemas: HashMap::new(),
56 }
57 }
58
59 pub fn description(mut self, desc: impl Into<String>) -> Self {
61 self.description = Some(desc.into());
62 self
63 }
64
65 pub fn register<T: ToSchema>(mut self) -> Self {
83 self.schemas
84 .insert(T::schema_name().to_string(), T::schema());
85 self
86 }
87
88 pub fn schema(mut self, name: impl Into<String>, schema: Schema) -> Self {
90 self.schemas.insert(name.into(), schema);
91 self
92 }
93
94 pub fn build(self) -> OpenApiSpec {
96 OpenApiSpec {
97 openapi: "3.0.0".to_string(),
98 info: Info {
99 title: self.title,
100 version: self.version,
101 description: self.description,
102 },
103 components: Components {
104 schemas: self.schemas,
105 },
106 }
107 }
108}
109
110impl OpenApiSpec {
111 pub fn builder(title: impl Into<String>, version: impl Into<String>) -> OpenApiBuilder {
113 OpenApiBuilder::new(title, version)
114 }
115
116 pub fn to_json(&self) -> Result<String, serde_json::Error> {
118 serde_json::to_string_pretty(self)
119 }
120
121 #[cfg(feature = "yaml")]
123 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
124 serde_yaml::to_string(self)
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 struct User;
133 impl ToSchema for User {
134 fn schema_name() -> &'static str {
135 "User"
136 }
137
138 fn schema() -> Schema {
139 Schema::object()
140 .property("email", Schema::string().format("email"))
141 .property("age", Schema::integer().minimum(0).maximum(150))
142 .required(&["email", "age"])
143 }
144 }
145
146 struct Product;
147 impl ToSchema for Product {
148 fn schema_name() -> &'static str {
149 "Product"
150 }
151
152 fn schema() -> Schema {
153 Schema::object()
154 .property("name", Schema::string())
155 .property("price", Schema::number().minimum(0.0))
156 .required(&["name", "price"])
157 }
158 }
159
160 #[test]
161 fn test_openapi_builder() {
162 let spec = OpenApiBuilder::new("Test API", "1.0.0")
163 .description("A test API")
164 .register::<User>()
165 .register::<Product>()
166 .build();
167
168 assert_eq!(spec.openapi, "3.0.0");
169 assert_eq!(spec.info.title, "Test API");
170 assert_eq!(spec.info.version, "1.0.0");
171 assert_eq!(spec.components.schemas.len(), 2);
172 assert!(spec.components.schemas.contains_key("User"));
173 assert!(spec.components.schemas.contains_key("Product"));
174 }
175
176 #[test]
177 fn test_to_json() {
178 let spec = OpenApiBuilder::new("API", "1.0").register::<User>().build();
179
180 let json = spec.to_json().unwrap();
181 assert!(json.contains("\"openapi\": \"3.0.0\""));
182 assert!(json.contains("\"User\""));
183 }
184
185 #[test]
186 fn test_manual_schema() {
187 let custom_schema = Schema::string().format("custom");
188
189 let spec = OpenApiBuilder::new("API", "1.0")
190 .schema("CustomType", custom_schema)
191 .build();
192
193 assert_eq!(spec.components.schemas.len(), 1);
194 assert!(spec.components.schemas.contains_key("CustomType"));
195 }
196}