diff --git a/src/stratum-api-s3/src/endpoint/definitions.rs b/src/stratum-api-s3/src/endpoint/definitions.rs index e69de29..73d2c2b 100644 --- a/src/stratum-api-s3/src/endpoint/definitions.rs +++ b/src/stratum-api-s3/src/endpoint/definitions.rs @@ -0,0 +1,53 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Endpoint { + // Service level + ListBuckets, + + // Bucket level + CreateBucket, + DeleteBucket, + HeadBucket, + ListObjectsV2 { + delimiter: Option, + prefix: Option, + max_keys: Option, + continuation_token: Option, + }, + + // Object level + GetObject { + key: String, + version_id: Option, + part_number: Option, + }, + PutObject { + key: String, + }, + DeleteObject { + key: String, + version_id: Option, + }, + HeadObject { + key: String, + version_id: Option, + part_number: Option, + }, + + // Multipart + CreateMultipartUpload { + key: String, + }, + UploadPart { + key: String, + part_number: u64, + upload_id: String, + }, + CompleteMultipartUpload { + key: String, + upload_id: String, + }, + AbortMultipartUpload { + key: String, + upload_id: String, + }, +} \ No newline at end of file diff --git a/src/stratum-api-s3/src/endpoint/mod.rs b/src/stratum-api-s3/src/endpoint/mod.rs index 1eb199d..d142ee9 100644 --- a/src/stratum-api-s3/src/endpoint/mod.rs +++ b/src/stratum-api-s3/src/endpoint/mod.rs @@ -1,3 +1,5 @@ mod definitions; mod parser; +pub use definitions::Endpoint; + diff --git a/src/stratum-api-s3/src/endpoint/parser.rs b/src/stratum-api-s3/src/endpoint/parser.rs index 7bf4cd7..c469934 100644 --- a/src/stratum-api-s3/src/endpoint/parser.rs +++ b/src/stratum-api-s3/src/endpoint/parser.rs @@ -1,3 +1,271 @@ use std::collections::HashMap; use hyper::Method; -use super::definitions:: \ No newline at end of file +use super::definitions::Endpoint; +use crate::errors::ApiError; + + +pub fn parse_endpoint( + method: &Method, + path: &str, + query: &HashMap, +) -> Result { + let segments: Vec<&str> = path + .trim_start_matches('/') + .splitn(2, '/') + .collect(); + + let bucket = segments.get(0).copied().unwrap_or(""); + let key = segments.get(1).copied().unwrap_or(""); + + match (method, bucket, key) { + // Service level + (&Method::GET, "", "") => Ok(Endpoint::ListBuckets), + + // Bucket level + (&Method::PUT, b, "") if !b.is_empty() => Ok(Endpoint::CreateBucket), + (&Method::DELETE, b, "") if !b.is_empty() => Ok(Endpoint::DeleteBucket), + (&Method::HEAD, b, "") if !b.is_empty() => Ok(Endpoint::HeadBucket), + (&Method::GET, b, "") if !b.is_empty() => Ok(Endpoint::ListObjectsV2 { + delimiter: query.get("delimiter").cloned(), + prefix: query.get("prefix").cloned(), + max_keys: query.get("max-keys") + .and_then(|v| v.parse().ok()), + continuation_token: query.get("continuation-token").cloned(), + }), + + // Object level + (&Method::GET, _, k) if !k.is_empty() => Ok(Endpoint::GetObject { + key: k.to_string(), + version_id: query.get("versionId").cloned(), + part_number: query.get("partNumber") + .and_then(|v| v.parse().ok()), + }), + (&Method::PUT, _, k) if !k.is_empty() => { + // distinguish UploadPart from PutObject + if let Some(upload_id) = query.get("uploadId") { + Ok(Endpoint::UploadPart { + key: k.to_string(), + upload_id: upload_id.clone(), + part_number: query.get("partNumber") + .and_then(|v| v.parse().ok()) + .ok_or(ApiError::InvalidArgument("missing partNumber".into()))?, + }) + } else { + Ok(Endpoint::PutObject { + key: k.to_string(), + }) + } + }, + (&Method::DELETE, _, k) if !k.is_empty() => { + if let Some(upload_id) = query.get("uploadId") { + Ok(Endpoint::AbortMultipartUpload { + key: k.to_string(), + upload_id: upload_id.clone(), + }) + } else { + Ok(Endpoint::DeleteObject { + key: k.to_string(), + version_id: query.get("versionId").cloned(), + }) + } + }, + (&Method::HEAD, _, k) if !k.is_empty() => Ok(Endpoint::HeadObject { + key: k.to_string(), + version_id: query.get("versionId").cloned(), + part_number: query.get("partNumber") + .and_then(|v| v.parse().ok()), + }), + (&Method::POST, _, k) if !k.is_empty() => { + if query.contains_key("uploads") { + Ok(Endpoint::CreateMultipartUpload { + key: k.to_string(), + }) + } else if let Some(upload_id) = query.get("uploadId") { + Ok(Endpoint::CompleteMultipartUpload { + key: k.to_string(), + upload_id: upload_id.clone(), + }) + } else { + Err(ApiError::InvalidArgument("unknown POST operation".into())) + } + }, + + _ => Err(ApiError::InvalidArgument( + format!("unknown endpoint: {} {}", method, path) + )), + } +} + +// src/stratum-api-s3/src/endpoint/parser.rs + +// ... your existing parse_endpoint function ... + +#[cfg(test)] +mod tests { + use super::*; + use hyper::Method; + use std::collections::HashMap; + + fn empty_query() -> HashMap { + HashMap::new() + } + + fn query(pairs: &[(&str, &str)]) -> HashMap { + pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect() + } + + // Service level + #[test] + fn test_list_buckets() { + let result = parse_endpoint(&Method::GET, "/", &empty_query()); + assert_eq!(result.unwrap(), Endpoint::ListBuckets); + } + + // Bucket level + #[test] + fn test_create_bucket() { + let result = parse_endpoint(&Method::PUT, "/my-bucket", &empty_query()); + assert_eq!(result.unwrap(), Endpoint::CreateBucket); + } + + #[test] + fn test_delete_bucket() { + let result = parse_endpoint(&Method::DELETE, "/my-bucket", &empty_query()); + assert_eq!(result.unwrap(), Endpoint::DeleteBucket); + } + + #[test] + fn test_head_bucket() { + let result = parse_endpoint(&Method::HEAD, "/my-bucket", &empty_query()); + assert_eq!(result.unwrap(), Endpoint::HeadBucket); + } + + #[test] + fn test_list_objects_v2_empty() { + let result = parse_endpoint(&Method::GET, "/my-bucket", &empty_query()); + assert_eq!(result.unwrap(), Endpoint::ListObjectsV2 { + delimiter: None, + prefix: None, + max_keys: None, + continuation_token: None, + }); + } + + #[test] + fn test_list_objects_v2_with_prefix() { + let q = query(&[("prefix", "photos/"), ("max-keys", "100")]); + let result = parse_endpoint(&Method::GET, "/my-bucket", &q); + assert_eq!(result.unwrap(), Endpoint::ListObjectsV2 { + delimiter: None, + prefix: Some("photos/".to_string()), + max_keys: Some(100), + continuation_token: None, + }); + } + + // Object level + #[test] + fn test_get_object() { + let result = parse_endpoint(&Method::GET, "/my-bucket/photo.jpg", &empty_query()); + assert_eq!(result.unwrap(), Endpoint::GetObject { + key: "photo.jpg".to_string(), + version_id: None, + part_number: None, + }); + } + + #[test] + fn test_get_object_nested_key() { + let result = parse_endpoint(&Method::GET, "/my-bucket/photos/2024/beach.jpg", &empty_query()); + assert_eq!(result.unwrap(), Endpoint::GetObject { + key: "photos/2024/beach.jpg".to_string(), // full path preserved + version_id: None, + part_number: None, + }); + } + + #[test] + fn test_put_object() { + let result = parse_endpoint(&Method::PUT, "/my-bucket/photo.jpg", &empty_query()); + assert_eq!(result.unwrap(), Endpoint::PutObject { + key: "photo.jpg".to_string(), + }); + } + + #[test] + fn test_delete_object() { + let result = parse_endpoint(&Method::DELETE, "/my-bucket/photo.jpg", &empty_query()); + assert_eq!(result.unwrap(), Endpoint::DeleteObject { + key: "photo.jpg".to_string(), + version_id: None, + }); + } + + #[test] + fn test_delete_object_with_version() { + let q = query(&[("versionId", "abc123")]); + let result = parse_endpoint(&Method::DELETE, "/my-bucket/photo.jpg", &q); + assert_eq!(result.unwrap(), Endpoint::DeleteObject { + key: "photo.jpg".to_string(), + version_id: Some("abc123".to_string()), + }); + } + + #[test] + fn test_head_object() { + let result = parse_endpoint(&Method::HEAD, "/my-bucket/photo.jpg", &empty_query()); + assert_eq!(result.unwrap(), Endpoint::HeadObject { + key: "photo.jpg".to_string(), + version_id: None, + part_number: None, + }); + } + + // Multipart + #[test] + fn test_create_multipart_upload() { + let q = query(&[("uploads", "")]); + let result = parse_endpoint(&Method::POST, "/my-bucket/video.mp4", &q); + assert_eq!(result.unwrap(), Endpoint::CreateMultipartUpload { + key: "video.mp4".to_string(), + }); + } + + #[test] + fn test_upload_part() { + let q = query(&[("partNumber", "1"), ("uploadId", "abc123")]); + let result = parse_endpoint(&Method::PUT, "/my-bucket/video.mp4", &q); + assert_eq!(result.unwrap(), Endpoint::UploadPart { + key: "video.mp4".to_string(), + part_number: 1, + upload_id: "abc123".to_string(), + }); + } + + #[test] + fn test_complete_multipart_upload() { + let q = query(&[("uploadId", "abc123")]); + let result = parse_endpoint(&Method::POST, "/my-bucket/video.mp4", &q); + assert_eq!(result.unwrap(), Endpoint::CompleteMultipartUpload { + key: "video.mp4".to_string(), + upload_id: "abc123".to_string(), + }); + } + + #[test] + fn test_abort_multipart_upload() { + let q = query(&[("uploadId", "abc123")]); + let result = parse_endpoint(&Method::DELETE, "/my-bucket/video.mp4", &q); + assert_eq!(result.unwrap(), Endpoint::AbortMultipartUpload { + key: "video.mp4".to_string(), + upload_id: "abc123".to_string(), + }); + } + + // Error cases + #[test] + fn test_unknown_endpoint_returns_error() { + let result = parse_endpoint(&Method::PATCH, "/my-bucket/photo.jpg", &empty_query()); + assert!(result.is_err()); + } +} \ No newline at end of file diff --git a/src/stratum-api-s3/src/errors/mod.rs b/src/stratum-api-s3/src/errors/mod.rs index d4a42f7..3853b4f 100644 --- a/src/stratum-api-s3/src/errors/mod.rs +++ b/src/stratum-api-s3/src/errors/mod.rs @@ -1 +1,3 @@ mod error; + +pub use error::ApiError; diff --git a/src/stratum-api-s3/src/lib.rs b/src/stratum-api-s3/src/lib.rs index 152f7ef..6e498b7 100644 --- a/src/stratum-api-s3/src/lib.rs +++ b/src/stratum-api-s3/src/lib.rs @@ -1,3 +1,4 @@ pub mod endpoint; -pub mod common; -pub mod errors; \ No newline at end of file +pub mod errors; + +pub use endpoint::Endpoint; \ No newline at end of file