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 definitions;
|
||||||
mod parser;
|
mod parser;
|
||||||
|
|
||||||
|
pub use definitions::Endpoint;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,271 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use hyper::Method;
|
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;
|
mod error;
|
||||||
|
|
||||||
|
pub use error::ApiError;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod endpoint;
|
pub mod endpoint;
|
||||||
pub mod common;
|
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
|
||||||
|
pub use endpoint::Endpoint;
|
||||||
Reference in New Issue
Block a user