use utoipa::{
openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme},
Modify, OpenApi,
};
#[derive(OpenApi)]
#[openapi(
info(
title = "rs3gw - High-Performance S3-Compatible Object Storage Gateway",
version = "0.1.0",
description = "A high-performance, S3-compatible object storage gateway with advanced features including S3 Select, GraphQL API, gRPC, WebSocket streaming, and ML-based caching.",
contact(
name = "rs3gw contributors",
url = "https://github.com/cool-japan/rs3gw"
),
license(
name = "MIT OR Apache-2.0",
url = "https://github.com/cool-japan/rs3gw/blob/main/LICENSE"
)
),
servers(
(url = "http://localhost:9000", description = "Local development server"),
(url = "https://s3.example.com", description = "Production server")
),
paths(
crate::api::handlers::health_check,
crate::api::handlers::list_buckets,
crate::api::handlers::head_bucket,
crate::api::handlers::create_bucket,
crate::api::handlers::delete_bucket,
crate::api::handlers::list_objects_v2,
crate::api::handlers::head_object,
crate::api::handlers::get_object,
crate::api::handlers::put_object,
crate::api::handlers::delete_object,
crate::api::handlers::copy_object,
crate::api::handlers::get_object_tagging,
crate::api::handlers::put_object_tagging,
),
components(schemas(
BucketInfo,
ObjectInfo,
ListBucketsResponse,
OwnerInfo,
ListObjectsResponse,
ErrorResponse,
SelectRequest,
InputSerialization,
CsvInput,
JsonInput,
ParquetInput,
OutputSerialization,
CsvOutput,
JsonOutput,
RequestProgress,
HealthResponse,
StorageStats,
StorageClassStats,
)),
modifiers(&SecurityAddon),
tags(
(name = "Buckets", description = "Bucket management operations"),
(name = "Objects", description = "Object storage operations"),
(name = "Multipart", description = "Multipart upload operations"),
(name = "S3 Select", description = "SQL queries on stored objects"),
(name = "GraphQL", description = "GraphQL query interface"),
(name = "Admin", description = "Administrative and monitoring endpoints")
)
)]
pub struct ApiDoc;
struct SecurityAddon;
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(components) = openapi.components.as_mut() {
components.add_security_scheme(
"aws_sigv4",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Basic)
.description(Some(
"AWS Signature Version 4 authentication. \
Provide Access Key ID and Secret Access Key."
.to_string(),
))
.build(),
),
);
components.add_security_scheme(
"api_key",
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("X-Amz-Access-Key-Id"))),
);
}
}
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct BucketInfo {
#[schema(example = "my-bucket")]
pub name: String,
#[schema(example = "2024-01-01T00:00:00Z")]
pub creation_date: String,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct ObjectInfo {
#[schema(example = "documents/readme.txt")]
pub key: String,
#[schema(example = "2024-01-01T12:00:00Z")]
pub last_modified: String,
#[schema(example = "\"d41d8cd98f00b204e9800998ecf8427e\"")]
pub etag: String,
#[schema(example = 1024)]
pub size: i64,
#[schema(example = "STANDARD")]
pub storage_class: String,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct ListBucketsResponse {
pub owner: OwnerInfo,
pub buckets: Vec<BucketInfo>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct OwnerInfo {
#[schema(example = "admin")]
pub display_name: String,
#[schema(example = "admin")]
pub id: String,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct ListObjectsResponse {
#[schema(example = "my-bucket")]
pub name: String,
#[schema(example = "documents/")]
pub prefix: Option<String>,
#[schema(example = 1000)]
pub max_keys: i32,
pub is_truncated: bool,
pub contents: Vec<ObjectInfo>,
pub next_continuation_token: Option<String>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct ErrorResponse {
#[schema(example = "NoSuchBucket")]
pub code: String,
#[schema(example = "The specified bucket does not exist")]
pub message: String,
#[schema(example = "/my-bucket")]
pub resource: Option<String>,
#[schema(example = "4442587FB7D0A2F9")]
pub request_id: Option<String>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct SelectRequest {
#[schema(example = "SELECT * FROM S3Object WHERE age > 25 LIMIT 10")]
pub expression: String,
#[schema(example = "SQL")]
pub expression_type: String,
pub input_serialization: InputSerialization,
pub output_serialization: OutputSerialization,
pub request_progress: Option<RequestProgress>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct InputSerialization {
pub csv: Option<CsvInput>,
pub json: Option<JsonInput>,
pub parquet: Option<ParquetInput>,
#[schema(example = "NONE")]
pub compression_type: Option<String>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct CsvInput {
#[schema(example = "USE")]
pub file_header_info: Option<String>,
#[schema(example = ",")]
pub field_delimiter: Option<String>,
#[schema(example = "\n")]
pub record_delimiter: Option<String>,
#[schema(example = "\"")]
pub quote_character: Option<String>,
#[schema(example = "\"")]
pub quote_escape_character: Option<String>,
pub comments: Option<String>,
pub allow_quoted_record_delimiter: Option<bool>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct JsonInput {
#[schema(example = "LINES")]
pub r#type: Option<String>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct ParquetInput {}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct OutputSerialization {
pub csv: Option<CsvOutput>,
pub json: Option<JsonOutput>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct CsvOutput {
#[schema(example = ",")]
pub field_delimiter: Option<String>,
#[schema(example = "\n")]
pub record_delimiter: Option<String>,
#[schema(example = "\"")]
pub quote_character: Option<String>,
#[schema(example = "\"")]
pub quote_escape_character: Option<String>,
#[schema(example = "ASNEEDED")]
pub quote_fields: Option<String>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct JsonOutput {
#[schema(example = "\n")]
pub record_delimiter: Option<String>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct RequestProgress {
pub enabled: bool,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct HealthResponse {
#[schema(example = "healthy")]
pub status: String,
#[schema(example = "2024-01-01T12:00:00Z")]
pub timestamp: String,
#[schema(example = 3600)]
pub uptime_seconds: u64,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct StorageStats {
#[schema(example = 5)]
pub bucket_count: usize,
#[schema(example = 1234)]
pub object_count: usize,
#[schema(example = 1073741824)]
pub total_bytes: u64,
pub storage_class_breakdown: std::collections::HashMap<String, StorageClassStats>,
}
#[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)]
pub struct StorageClassStats {
#[schema(example = 100)]
pub object_count: usize,
#[schema(example = 104857600)]
pub total_bytes: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_openapi_schema_generation() {
let doc = ApiDoc::openapi();
assert_eq!(
doc.info.title,
"rs3gw - High-Performance S3-Compatible Object Storage Gateway"
);
assert_eq!(doc.info.version, "0.1.0");
assert!(doc.servers.is_some());
assert!(!doc.servers.expect("Failed to get servers").is_empty());
}
#[test]
fn test_security_schemes() {
let doc = ApiDoc::openapi();
let components = doc.components.as_ref().expect("Failed to get components");
assert!(components.security_schemes.contains_key("aws_sigv4"));
assert!(components.security_schemes.contains_key("api_key"));
}
#[test]
fn test_schema_definitions() {
let doc = ApiDoc::openapi();
let components = doc.components.as_ref().expect("Failed to get components");
let schema_names: Vec<&str> = components.schemas.keys().map(|s| s.as_str()).collect();
let expected = vec![
"BucketInfo",
"ObjectInfo",
"ListBucketsResponse",
"ErrorResponse",
"HealthResponse",
];
for name in expected {
assert!(
schema_names.contains(&name),
"Schema '{}' should be present in components",
name
);
}
}
#[test]
fn test_paths_included() {
let doc = ApiDoc::openapi();
assert!(!doc.paths.paths.is_empty(), "Paths should not be empty");
assert!(
doc.paths.paths.contains_key("/health"),
"/health path should be present"
);
assert!(
doc.paths.paths.contains_key("/"),
"/ (list buckets) path should be present"
);
}
}