use crate::{Schema, ToSchema};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenApiSpec {
pub openapi: String,
pub info: Info,
pub components: Components,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Info {
pub title: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Components {
pub schemas: HashMap<String, Schema>,
}
pub struct OpenApiBuilder {
title: String,
version: String,
description: Option<String>,
schemas: HashMap<String, Schema>,
}
impl OpenApiBuilder {
pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
Self {
title: title.into(),
version: version.into(),
description: None,
schemas: HashMap::new(),
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn register<T: ToSchema>(mut self) -> Self {
self.schemas
.insert(T::schema_name().to_string(), T::schema());
self
}
pub fn schema(mut self, name: impl Into<String>, schema: Schema) -> Self {
self.schemas.insert(name.into(), schema);
self
}
pub fn build(self) -> OpenApiSpec {
OpenApiSpec {
openapi: "3.0.0".to_string(),
info: Info {
title: self.title,
version: self.version,
description: self.description,
},
components: Components {
schemas: self.schemas,
},
}
}
}
impl OpenApiSpec {
pub fn builder(title: impl Into<String>, version: impl Into<String>) -> OpenApiBuilder {
OpenApiBuilder::new(title, version)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
#[cfg(feature = "yaml")]
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct User;
impl ToSchema for User {
fn schema_name() -> &'static str {
"User"
}
fn schema() -> Schema {
Schema::object()
.property("email", Schema::string().format("email"))
.property("age", Schema::integer().minimum(0).maximum(150))
.required(&["email", "age"])
}
}
struct Product;
impl ToSchema for Product {
fn schema_name() -> &'static str {
"Product"
}
fn schema() -> Schema {
Schema::object()
.property("name", Schema::string())
.property("price", Schema::number().minimum(0.0))
.required(&["name", "price"])
}
}
#[test]
fn test_openapi_builder() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.description("A test API")
.register::<User>()
.register::<Product>()
.build();
assert_eq!(spec.openapi, "3.0.0");
assert_eq!(spec.info.title, "Test API");
assert_eq!(spec.info.version, "1.0.0");
assert_eq!(spec.components.schemas.len(), 2);
assert!(spec.components.schemas.contains_key("User"));
assert!(spec.components.schemas.contains_key("Product"));
}
#[test]
fn test_to_json() {
let spec = OpenApiBuilder::new("API", "1.0").register::<User>().build();
let json = spec.to_json().unwrap();
assert!(json.contains("\"openapi\": \"3.0.0\""));
assert!(json.contains("\"User\""));
}
#[test]
fn test_manual_schema() {
let custom_schema = Schema::string().format("custom");
let spec = OpenApiBuilder::new("API", "1.0")
.schema("CustomType", custom_schema)
.build();
assert_eq!(spec.components.schemas.len(), 1);
assert!(spec.components.schemas.contains_key("CustomType"));
}
}