use crate::Result;
use crate::error::OpenApiError;
use crate::types::{OpenApiPath, OpenApiSchema};
use serde_json::{Map, Value};
#[derive(Debug, Clone)]
pub struct OpenApiBuilder {
title: Option<String>,
version: Option<String>,
description: Option<String>,
paths: Map<String, Value>,
schemas: Map<String, Value>,
}
impl Default for OpenApiBuilder {
fn default() -> Self {
Self::new()
}
}
impl OpenApiBuilder {
pub fn new() -> Self {
Self {
title: None,
version: None,
description: None,
paths: Map::new(),
schemas: Map::new(),
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn merge(mut self, spec: Value) -> Result<Self> {
if let Some(paths) = spec.get("paths").and_then(|p| p.as_object()) {
for (path, methods) in paths {
if let Some(methods_obj) = methods.as_object() {
let path_entry = self
.paths
.entry(path.clone())
.or_insert_with(|| Value::Object(Map::new()));
if let Some(path_obj) = path_entry.as_object_mut() {
for (method, operation) in methods_obj {
path_obj.insert(method.clone(), operation.clone());
}
}
}
}
}
if let Some(components) = spec.get("components").and_then(|c| c.as_object())
&& let Some(schemas) = components.get("schemas").and_then(|s| s.as_object())
{
for (name, schema) in schemas {
self.merge_schema(name.clone(), schema.clone())?;
}
}
if let Some(schemas) = spec.get("schemas").and_then(|s| s.as_object()) {
for (name, schema) in schemas {
self.merge_schema(name.clone(), schema.clone())?;
}
}
Ok(self)
}
pub fn merge_paths(mut self, paths: Vec<OpenApiPath>) -> Self {
for path_def in paths {
let path_entry = self
.paths
.entry(path_def.path.clone())
.or_insert_with(|| Value::Object(Map::new()));
if let Some(path_obj) = path_entry.as_object_mut() {
let operation = serde_json::to_value(&path_def.operation)
.unwrap_or_else(|_| Value::Object(Map::new()));
path_obj.insert(path_def.method.to_lowercase(), operation);
}
}
self
}
pub fn merge_schemas(mut self, schemas: Vec<OpenApiSchema>) -> Result<Self> {
for schema_def in schemas {
self.merge_schema(schema_def.name, schema_def.schema)?;
}
Ok(self)
}
fn merge_schema(&mut self, name: String, schema: Value) -> Result<()> {
if let Some(existing) = self.schemas.get(&name) {
if existing != &schema {
return Err(OpenApiError::SchemaConflict { name });
}
} else {
self.schemas.insert(name, schema);
}
Ok(())
}
pub fn build(self) -> Value {
let mut spec = Map::new();
spec.insert("openapi".to_string(), Value::String("3.0.0".to_string()));
let mut info = Map::new();
info.insert(
"title".to_string(),
Value::String(self.title.unwrap_or_else(|| "API".to_string())),
);
info.insert(
"version".to_string(),
Value::String(self.version.unwrap_or_else(|| "0.1.0".to_string())),
);
if let Some(desc) = self.description {
info.insert("description".to_string(), Value::String(desc));
}
spec.insert("info".to_string(), Value::Object(info));
if !self.paths.is_empty() {
spec.insert("paths".to_string(), Value::Object(self.paths));
}
if !self.schemas.is_empty() {
let mut components = Map::new();
components.insert("schemas".to_string(), Value::Object(self.schemas));
spec.insert("components".to_string(), Value::Object(components));
}
Value::Object(spec)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_basic_builder() {
let spec = OpenApiBuilder::new()
.title("Test API")
.version("1.0.0")
.description("A test API")
.build();
assert_eq!(spec["info"]["title"], "Test API");
assert_eq!(spec["info"]["version"], "1.0.0");
assert_eq!(spec["info"]["description"], "A test API");
assert_eq!(spec["openapi"], "3.0.0");
}
#[test]
fn test_merge_paths() {
let spec1 = json!({
"paths": {
"/users": {
"get": {"summary": "List users"}
}
}
});
let spec2 = json!({
"paths": {
"/orders": {
"get": {"summary": "List orders"}
}
}
});
let combined = OpenApiBuilder::new()
.merge(spec1)
.unwrap()
.merge(spec2)
.unwrap()
.build();
assert!(combined["paths"]["/users"]["get"].is_object());
assert!(combined["paths"]["/orders"]["get"].is_object());
}
#[test]
fn test_path_override() {
let spec1 = json!({
"paths": {
"/users": {
"get": {"summary": "First"}
}
}
});
let spec2 = json!({
"paths": {
"/users": {
"get": {"summary": "Second"}
}
}
});
let combined = OpenApiBuilder::new()
.merge(spec1)
.unwrap()
.merge(spec2)
.unwrap()
.build();
assert_eq!(combined["paths"]["/users"]["get"]["summary"], "Second");
}
#[test]
fn test_schema_deduplication() {
let spec1 = json!({
"components": {
"schemas": {
"User": {"type": "object", "properties": {"name": {"type": "string"}}}
}
}
});
let spec2 = json!({
"components": {
"schemas": {
"User": {"type": "object", "properties": {"name": {"type": "string"}}}
}
}
});
let result = OpenApiBuilder::new().merge(spec1).unwrap().merge(spec2);
assert!(result.is_ok());
let combined = result.unwrap().build();
assert!(combined["components"]["schemas"]["User"].is_object());
}
#[test]
fn test_schema_conflict() {
let spec1 = json!({
"components": {
"schemas": {
"User": {"type": "object", "properties": {"name": {"type": "string"}}}
}
}
});
let spec2 = json!({
"components": {
"schemas": {
"User": {"type": "object", "properties": {"id": {"type": "integer"}}}
}
}
});
let result = OpenApiBuilder::new().merge(spec1).unwrap().merge(spec2);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, OpenApiError::SchemaConflict { name } if name == "User"));
}
#[test]
fn test_merge_typed_paths() {
use crate::types::{OpenApiOperation, OpenApiPath};
let paths = vec![
OpenApiPath::new("/users", "get").with_operation(OpenApiOperation::new("List users")),
OpenApiPath::new("/users", "post").with_operation(OpenApiOperation::new("Create user")),
];
let spec = OpenApiBuilder::new()
.title("Test")
.merge_paths(paths)
.build();
assert_eq!(spec["paths"]["/users"]["get"]["summary"], "List users");
assert_eq!(spec["paths"]["/users"]["post"]["summary"], "Create user");
}
#[test]
fn test_merge_typed_schemas() {
use crate::types::OpenApiSchema;
let schemas = vec![OpenApiSchema::new(
"User",
json!({"type": "object", "properties": {"name": {"type": "string"}}}),
)];
let spec = OpenApiBuilder::new()
.title("Test")
.merge_schemas(schemas)
.unwrap()
.build();
assert!(spec["components"]["schemas"]["User"].is_object());
}
}