server_less_openapi/
builder.rs1use crate::Result;
4use crate::error::OpenApiError;
5use crate::types::{OpenApiPath, OpenApiSchema};
6use serde_json::{Map, Value};
7
8#[derive(Debug, Clone)]
28pub struct OpenApiBuilder {
29 title: Option<String>,
30 version: Option<String>,
31 description: Option<String>,
32 paths: Map<String, Value>,
33 schemas: Map<String, Value>,
34}
35
36impl Default for OpenApiBuilder {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl OpenApiBuilder {
43 pub fn new() -> Self {
45 Self {
46 title: None,
47 version: None,
48 description: None,
49 paths: Map::new(),
50 schemas: Map::new(),
51 }
52 }
53
54 pub fn title(mut self, title: impl Into<String>) -> Self {
56 self.title = Some(title.into());
57 self
58 }
59
60 pub fn version(mut self, version: impl Into<String>) -> Self {
62 self.version = Some(version.into());
63 self
64 }
65
66 pub fn description(mut self, description: impl Into<String>) -> Self {
68 self.description = Some(description.into());
69 self
70 }
71
72 pub fn merge(mut self, spec: Value) -> Result<Self> {
81 if let Some(paths) = spec.get("paths").and_then(|p| p.as_object()) {
83 for (path, methods) in paths {
84 if let Some(methods_obj) = methods.as_object() {
85 let path_entry = self
86 .paths
87 .entry(path.clone())
88 .or_insert_with(|| Value::Object(Map::new()));
89
90 if let Some(path_obj) = path_entry.as_object_mut() {
91 for (method, operation) in methods_obj {
92 path_obj.insert(method.clone(), operation.clone());
94 }
95 }
96 }
97 }
98 }
99
100 if let Some(components) = spec.get("components").and_then(|c| c.as_object())
102 && let Some(schemas) = components.get("schemas").and_then(|s| s.as_object())
103 {
104 for (name, schema) in schemas {
105 self.merge_schema(name.clone(), schema.clone())?;
106 }
107 }
108
109 if let Some(schemas) = spec.get("schemas").and_then(|s| s.as_object()) {
111 for (name, schema) in schemas {
112 self.merge_schema(name.clone(), schema.clone())?;
113 }
114 }
115
116 Ok(self)
117 }
118
119 pub fn merge_paths(mut self, paths: Vec<OpenApiPath>) -> Self {
121 for path_def in paths {
122 let path_entry = self
123 .paths
124 .entry(path_def.path.clone())
125 .or_insert_with(|| Value::Object(Map::new()));
126
127 if let Some(path_obj) = path_entry.as_object_mut() {
128 let operation = serde_json::to_value(&path_def.operation)
130 .unwrap_or_else(|_| Value::Object(Map::new()));
131 path_obj.insert(path_def.method.to_lowercase(), operation);
132 }
133 }
134 self
135 }
136
137 pub fn merge_schemas(mut self, schemas: Vec<OpenApiSchema>) -> Result<Self> {
139 for schema_def in schemas {
140 self.merge_schema(schema_def.name, schema_def.schema)?;
141 }
142 Ok(self)
143 }
144
145 fn merge_schema(&mut self, name: String, schema: Value) -> Result<()> {
147 if let Some(existing) = self.schemas.get(&name) {
148 if existing != &schema {
150 return Err(OpenApiError::SchemaConflict { name });
151 }
152 } else {
154 self.schemas.insert(name, schema);
155 }
156 Ok(())
157 }
158
159 pub fn build(self) -> Value {
161 let mut spec = Map::new();
162
163 spec.insert("openapi".to_string(), Value::String("3.0.0".to_string()));
165
166 let mut info = Map::new();
168 info.insert(
169 "title".to_string(),
170 Value::String(self.title.unwrap_or_else(|| "API".to_string())),
171 );
172 info.insert(
173 "version".to_string(),
174 Value::String(self.version.unwrap_or_else(|| "0.1.0".to_string())),
175 );
176 if let Some(desc) = self.description {
177 info.insert("description".to_string(), Value::String(desc));
178 }
179 spec.insert("info".to_string(), Value::Object(info));
180
181 if !self.paths.is_empty() {
183 spec.insert("paths".to_string(), Value::Object(self.paths));
184 }
185
186 if !self.schemas.is_empty() {
188 let mut components = Map::new();
189 components.insert("schemas".to_string(), Value::Object(self.schemas));
190 spec.insert("components".to_string(), Value::Object(components));
191 }
192
193 Value::Object(spec)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use serde_json::json;
201
202 #[test]
203 fn test_basic_builder() {
204 let spec = OpenApiBuilder::new()
205 .title("Test API")
206 .version("1.0.0")
207 .description("A test API")
208 .build();
209
210 assert_eq!(spec["info"]["title"], "Test API");
211 assert_eq!(spec["info"]["version"], "1.0.0");
212 assert_eq!(spec["info"]["description"], "A test API");
213 assert_eq!(spec["openapi"], "3.0.0");
214 }
215
216 #[test]
217 fn test_merge_paths() {
218 let spec1 = json!({
219 "paths": {
220 "/users": {
221 "get": {"summary": "List users"}
222 }
223 }
224 });
225
226 let spec2 = json!({
227 "paths": {
228 "/orders": {
229 "get": {"summary": "List orders"}
230 }
231 }
232 });
233
234 let combined = OpenApiBuilder::new()
235 .merge(spec1)
236 .unwrap()
237 .merge(spec2)
238 .unwrap()
239 .build();
240
241 assert!(combined["paths"]["/users"]["get"].is_object());
242 assert!(combined["paths"]["/orders"]["get"].is_object());
243 }
244
245 #[test]
246 fn test_path_override() {
247 let spec1 = json!({
248 "paths": {
249 "/users": {
250 "get": {"summary": "First"}
251 }
252 }
253 });
254
255 let spec2 = json!({
256 "paths": {
257 "/users": {
258 "get": {"summary": "Second"}
259 }
260 }
261 });
262
263 let combined = OpenApiBuilder::new()
264 .merge(spec1)
265 .unwrap()
266 .merge(spec2)
267 .unwrap()
268 .build();
269
270 assert_eq!(combined["paths"]["/users"]["get"]["summary"], "Second");
272 }
273
274 #[test]
275 fn test_schema_deduplication() {
276 let spec1 = json!({
277 "components": {
278 "schemas": {
279 "User": {"type": "object", "properties": {"name": {"type": "string"}}}
280 }
281 }
282 });
283
284 let spec2 = json!({
285 "components": {
286 "schemas": {
287 "User": {"type": "object", "properties": {"name": {"type": "string"}}}
288 }
289 }
290 });
291
292 let result = OpenApiBuilder::new().merge(spec1).unwrap().merge(spec2);
294 assert!(result.is_ok());
295
296 let combined = result.unwrap().build();
297 assert!(combined["components"]["schemas"]["User"].is_object());
298 }
299
300 #[test]
301 fn test_schema_conflict() {
302 let spec1 = json!({
303 "components": {
304 "schemas": {
305 "User": {"type": "object", "properties": {"name": {"type": "string"}}}
306 }
307 }
308 });
309
310 let spec2 = json!({
311 "components": {
312 "schemas": {
313 "User": {"type": "object", "properties": {"id": {"type": "integer"}}}
314 }
315 }
316 });
317
318 let result = OpenApiBuilder::new().merge(spec1).unwrap().merge(spec2);
320 assert!(result.is_err());
321
322 let err = result.unwrap_err();
323 assert!(matches!(err, OpenApiError::SchemaConflict { name } if name == "User"));
324 }
325
326 #[test]
327 fn test_merge_typed_paths() {
328 use crate::types::{OpenApiOperation, OpenApiPath};
329
330 let paths = vec![
331 OpenApiPath::new("/users", "get").with_operation(OpenApiOperation::new("List users")),
332 OpenApiPath::new("/users", "post").with_operation(OpenApiOperation::new("Create user")),
333 ];
334
335 let spec = OpenApiBuilder::new()
336 .title("Test")
337 .merge_paths(paths)
338 .build();
339
340 assert_eq!(spec["paths"]["/users"]["get"]["summary"], "List users");
341 assert_eq!(spec["paths"]["/users"]["post"]["summary"], "Create user");
342 }
343
344 #[test]
345 fn test_merge_typed_schemas() {
346 use crate::types::OpenApiSchema;
347
348 let schemas = vec![OpenApiSchema::new(
349 "User",
350 json!({"type": "object", "properties": {"name": {"type": "string"}}}),
351 )];
352
353 let spec = OpenApiBuilder::new()
354 .title("Test")
355 .merge_schemas(schemas)
356 .unwrap()
357 .build();
358
359 assert!(spec["components"]["schemas"]["User"].is_object());
360 }
361}