Implemented methods as defined in the definition + tests
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Endpoint {
|
||||
// Service level
|
||||
ListBuckets,
|
||||
|
||||
// Bucket level
|
||||
CreateBucket,
|
||||
DeleteBucket,
|
||||
HeadBucket,
|
||||
ListObjectsV2 {
|
||||
delimiter: Option<String>,
|
||||
prefix: Option<String>,
|
||||
max_keys: Option<usize>,
|
||||
continuation_token: Option<String>,
|
||||
},
|
||||
|
||||
// Object level
|
||||
GetObject {
|
||||
key: String,
|
||||
version_id: Option<String>,
|
||||
part_number: Option<u64>,
|
||||
},
|
||||
PutObject {
|
||||
key: String,
|
||||
},
|
||||
DeleteObject {
|
||||
key: String,
|
||||
version_id: Option<String>,
|
||||
},
|
||||
HeadObject {
|
||||
key: String,
|
||||
version_id: Option<String>,
|
||||
part_number: Option<u64>,
|
||||
},
|
||||
|
||||
// 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,
|
||||
},
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod definitions;
|
||||
mod parser;
|
||||
|
||||
pub use definitions::Endpoint;
|
||||
|
||||
|
||||
@@ -1,3 +1,271 @@
|
||||
use std::collections::HashMap;
|
||||
use hyper::Method;
|
||||
use super::definitions::
|
||||
use super::definitions::Endpoint;
|
||||
use crate::errors::ApiError;
|
||||
|
||||
|
||||
pub fn parse_endpoint(
|
||||
method: &Method,
|
||||
path: &str,
|
||||
query: &HashMap<String, String>,
|
||||
) -> Result<Endpoint, ApiError> {
|
||||
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<String, String> {
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
fn query(pairs: &[(&str, &str)]) -> HashMap<String, String> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
mod error;
|
||||
|
||||
pub use error::ApiError;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod endpoint;
|
||||
pub mod common;
|
||||
pub mod errors;
|
||||
pub mod errors;
|
||||
|
||||
pub use endpoint::Endpoint;
|
||||
Reference in New Issue
Block a user