use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
info(
title = "usenet-dl REST API",
version = "0.1.0",
description = "OpenAPI 3.1 compliant REST API for managing Usenet downloads, post-processing, and configuration",
contact(
name = "usenet-dl",
url = "https://github.com/jvz-devx/usenet-dl"
),
license(
name = "MIT OR Apache-2.0"
)
),
servers(
(url = "http://localhost:6789/api/v1", description = "Local development server")
),
paths(
// Queue Management - Downloads
crate::api::routes::list_downloads,
crate::api::routes::get_download,
crate::api::routes::add_download,
crate::api::routes::add_download_url,
crate::api::routes::pause_download,
crate::api::routes::resume_download,
crate::api::routes::delete_download,
crate::api::routes::set_download_priority,
crate::api::routes::reprocess_download,
crate::api::routes::reextract_download,
// Queue-Wide Operations
crate::api::routes::pause_queue,
crate::api::routes::resume_queue,
crate::api::routes::queue_stats,
// History
crate::api::routes::get_history,
crate::api::routes::clear_history,
// Server Management
crate::api::routes::test_server,
crate::api::routes::test_all_servers,
// Configuration
crate::api::routes::get_config,
crate::api::routes::update_config,
crate::api::routes::get_speed_limit,
crate::api::routes::set_speed_limit,
// Categories
crate::api::routes::list_categories,
crate::api::routes::create_or_update_category,
crate::api::routes::delete_category,
// System
crate::api::routes::get_capabilities,
crate::api::routes::health_check,
crate::api::routes::openapi_spec,
crate::api::routes::event_stream,
crate::api::routes::shutdown,
// RSS Feeds
crate::api::routes::list_rss_feeds,
crate::api::routes::add_rss_feed,
crate::api::routes::update_rss_feed,
crate::api::routes::delete_rss_feed,
crate::api::routes::check_rss_feed,
// Scheduler
crate::api::routes::list_schedule_rules,
crate::api::routes::add_schedule_rule,
crate::api::routes::update_schedule_rule,
crate::api::routes::delete_schedule_rule,
),
components(schemas(
// Core types from types.rs
crate::types::Status,
crate::types::Priority,
crate::types::Stage,
crate::types::ArchiveType,
crate::types::DownloadInfo,
crate::types::DownloadOptions,
crate::types::HistoryEntry,
crate::types::QueueStats,
crate::types::Capabilities,
crate::types::ParityCapabilitiesInfo,
crate::types::ServerCapabilities,
// Config types from config.rs
crate::config::Config,
crate::config::ConfigUpdate,
crate::config::ServerConfig,
crate::config::RetryConfig,
crate::config::PostProcess,
crate::config::ExtractionConfig,
crate::config::FileCollisionAction,
crate::config::DeobfuscationConfig,
crate::config::DuplicateConfig,
crate::config::DuplicateAction,
crate::config::DuplicateMethod,
crate::config::DiskSpaceConfig,
crate::config::CleanupConfig,
crate::config::ApiConfig,
crate::config::RateLimitConfig,
crate::config::ScheduleRule,
crate::config::ScheduleAction,
crate::config::Weekday,
crate::config::WatchFolderConfig,
crate::config::WatchFolderAction,
crate::config::WebhookConfig,
crate::config::WebhookEvent,
crate::config::ScriptConfig,
crate::config::ScriptEvent,
crate::config::CategoryConfig,
crate::config::RssFeedConfig,
crate::config::RssFilter,
// API request/response types from routes.rs
crate::api::routes::AddRssFeedRequest,
crate::api::routes::RssFeedResponse,
crate::api::routes::CheckRssFeedResponse,
crate::api::routes::ScheduleRuleResponse,
// Error types from error.rs
crate::error::ApiError,
crate::error::ErrorDetail,
)),
tags(
(name = "downloads", description = "Download queue management - Add, pause, resume, and monitor downloads"),
(name = "queue", description = "Queue-wide operations - Pause/resume all downloads, get statistics"),
(name = "history", description = "Download history - View completed and failed downloads"),
(name = "servers", description = "Server management - Test NNTP server connections and configuration"),
(name = "config", description = "Configuration - Get and update runtime configuration settings"),
(name = "categories", description = "Categories - Manage download categories and their settings"),
(name = "system", description = "System endpoints - Health checks, OpenAPI spec, events, shutdown"),
(name = "rss", description = "RSS feeds - Manage RSS feed subscriptions and automatic downloads"),
(name = "scheduler", description = "Scheduler - Time-based rules for speed limits and pause/resume"),
),
modifiers(&SecurityAddon)
)]
pub struct ApiDoc;
struct SecurityAddon;
impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(components) = &mut openapi.components {
components.add_security_scheme(
"api_key",
utoipa::openapi::security::SecurityScheme::ApiKey(
utoipa::openapi::security::ApiKey::Header(
utoipa::openapi::security::ApiKeyValue::new("X-Api-Key"),
),
),
);
}
}
}
#[allow(clippy::unwrap_used, clippy::expect_used)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_openapi_doc_generation() {
let _spec = ApiDoc::openapi();
}
#[test]
fn test_openapi_spec_has_paths() {
let spec = ApiDoc::openapi();
assert!(
!spec.paths.paths.is_empty(),
"OpenAPI spec should have paths defined"
);
}
#[test]
fn test_openapi_spec_has_components() {
let spec = ApiDoc::openapi();
assert!(
spec.components.is_some(),
"OpenAPI spec should have components defined"
);
let components = spec.components.unwrap();
assert!(
!components.schemas.is_empty(),
"OpenAPI spec should have schemas defined"
);
}
#[test]
fn test_openapi_spec_has_tags() {
let spec = ApiDoc::openapi();
assert!(spec.tags.is_some(), "OpenAPI spec should have tags defined");
let tags = spec.tags.unwrap();
assert!(
!tags.is_empty(),
"OpenAPI spec should have at least one tag"
);
let tag_names: Vec<&str> = tags.iter().map(|t| t.name.as_str()).collect();
assert!(
tag_names.contains(&"downloads"),
"Should have 'downloads' tag"
);
assert!(tag_names.contains(&"queue"), "Should have 'queue' tag");
assert!(tag_names.contains(&"config"), "Should have 'config' tag");
assert!(tag_names.contains(&"system"), "Should have 'system' tag");
}
#[test]
fn test_openapi_spec_info() {
let spec = ApiDoc::openapi();
assert_eq!(spec.info.title, "usenet-dl REST API");
assert_eq!(spec.info.version, "0.1.0");
assert!(spec.info.description.is_some());
}
#[test]
fn test_openapi_spec_has_security_scheme() {
let spec = ApiDoc::openapi();
assert!(spec.components.is_some());
let components = spec.components.unwrap();
assert!(
components.security_schemes.contains_key("api_key"),
"Should have 'api_key' security scheme defined"
);
}
#[test]
fn test_openapi_json_serialization() {
let spec = ApiDoc::openapi();
let json = serde_json::to_string(&spec).expect("Should serialize to JSON");
assert!(!json.is_empty(), "JSON output should not be empty");
let _value: serde_json::Value =
serde_json::from_str(&json).expect("Generated JSON should be valid");
}
#[test]
fn test_openapi_spec_version() {
let spec = ApiDoc::openapi();
let json = serde_json::to_value(&spec).expect("Should serialize to JSON");
let version = json.get("openapi").and_then(|v| v.as_str());
assert!(version.is_some(), "Should have openapi version field");
assert!(
version.unwrap().starts_with("3."),
"Should use OpenAPI 3.x version"
);
}
}