pub(super) const DEFAULT_SPEC_PATH: &str = "/openapi.json";
const DEFAULT_SPEC_NAME: &str = "v1";
const DEFAULT_UI_PATH: &str = "/openapi";
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(default)]
pub struct OpenApiConfig {
#[serde(skip)]
pub exposed: bool,
pub(super) title: String,
pub(super) version: String,
pub(super) description: Option<String>,
pub(super) specs: Vec<OpenApiSpec>,
pub(super) ui_enabled: bool,
pub(super) ui_path: String,
}
#[derive(Clone, Debug, serde::Deserialize)]
pub struct OpenApiSpec {
pub name: String,
pub spec_path: String,
}
impl Default for OpenApiConfig {
fn default() -> Self {
Self {
exposed: false,
title: "Volga API".to_string(),
version: "0.1.0".to_string(),
description: None,
specs: vec![OpenApiSpec::default()],
ui_enabled: false,
ui_path: DEFAULT_UI_PATH.to_string(),
}
}
}
impl Default for OpenApiSpec {
fn default() -> Self {
Self {
name: DEFAULT_SPEC_NAME.to_string(),
spec_path: DEFAULT_SPEC_PATH.to_string(),
}
}
}
impl<T: Into<String>> From<T> for OpenApiSpec {
#[inline]
fn from(value: T) -> Self {
Self::new(value)
}
}
impl OpenApiSpec {
#[inline]
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
Self {
spec_path: format!("/{name}{DEFAULT_SPEC_PATH}"),
name,
}
}
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.spec_path = path.into();
self
}
}
impl OpenApiConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = version.into();
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_spec(mut self, spec: OpenApiSpec) -> Self {
self.specs = vec![spec];
self
}
pub fn with_specs<I, S>(mut self, specs: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<OpenApiSpec>,
{
let specs: Vec<_> = specs.into_iter().map(Into::into).collect();
self.specs = if specs.is_empty() {
vec![OpenApiSpec::default()]
} else {
specs
};
self
}
pub fn with_ui(mut self) -> Self {
self.ui_enabled = true;
self
}
pub fn with_ui_path(mut self, path: impl Into<String>) -> Self {
self.ui_path = path.into();
self
}
pub fn title(&self) -> &str {
&self.title
}
pub fn specs(&self) -> &[OpenApiSpec] {
&self.specs
}
pub fn ui_enabled(&self) -> bool {
self.ui_enabled
}
pub fn ui_path(&self) -> &str {
&self.ui_path
}
}
#[cfg(test)]
mod tests {
use super::{DEFAULT_SPEC_PATH, OpenApiConfig, OpenApiSpec};
#[test]
fn default_config_contains_single_default_spec_and_ui_disabled() {
let config = OpenApiConfig::default();
assert_eq!(config.title(), "Volga API");
assert_eq!(config.specs().len(), 1);
assert_eq!(config.specs()[0].name, "v1");
assert_eq!(config.specs()[0].spec_path, DEFAULT_SPEC_PATH);
assert!(!config.ui_enabled());
assert_eq!(config.ui_path(), "/openapi");
}
#[test]
fn spec_new_builds_path_from_spec_name() {
let spec = OpenApiSpec::new("admin");
assert_eq!(spec.name, "admin");
assert_eq!(spec.spec_path, "/admin/openapi.json");
}
#[test]
fn with_specs_accepts_mixed_inputs() {
let config = OpenApiConfig::new().with_specs([
OpenApiSpec::new("v1").with_path("/docs/v1.json"),
OpenApiSpec::from("v2"),
]);
assert_eq!(config.specs().len(), 2);
assert_eq!(config.specs()[0].spec_path, "/docs/v1.json");
assert_eq!(config.specs()[1].name, "v2");
assert_eq!(config.specs()[1].spec_path, "/v2/openapi.json");
}
#[test]
fn with_spec_replaces_existing_specs() {
let config = OpenApiConfig::new().with_spec(OpenApiSpec::new("admin"));
assert_eq!(config.specs().len(), 1);
assert_eq!(config.specs()[0].name, "admin");
assert_eq!(config.specs()[0].spec_path, "/admin/openapi.json");
}
#[test]
fn with_title_version_and_description_override_defaults() {
let config = OpenApiConfig::new()
.with_title("Custom API")
.with_version("2.5.1")
.with_description("custom description");
assert_eq!(config.title(), "Custom API");
assert_eq!(config.version, "2.5.1");
assert_eq!(config.description.as_deref(), Some("custom description"));
}
#[test]
fn with_specs_restores_default_when_given_empty_list() {
let config = OpenApiConfig::new().with_specs(Vec::<OpenApiSpec>::new());
assert_eq!(config.specs().len(), 1);
assert_eq!(config.specs()[0].name, "v1");
assert_eq!(config.specs()[0].spec_path, DEFAULT_SPEC_PATH);
}
}