cobalt_aws/s3/
s3_object.rs1use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
6use url::Url;
7
8#[derive(Debug, thiserror::Error)]
9#[allow(clippy::enum_variant_names)]
10pub enum S3ObjectError {
11 #[error("S3 URL must have a scheme of s3")]
12 UrlSchemeError,
13 #[error("S3 URL must have a host")]
14 UrlHostError,
15 #[error("S3 URL must have a path")]
16 UrlPathError,
17 #[error(transparent)]
18 UrlParseError(#[from] url::ParseError),
19}
20
21#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
23pub struct S3Object {
24 pub bucket: String,
26 pub key: String,
28}
29
30impl S3Object {
31 pub fn new(bucket: impl AsRef<str>, key: impl AsRef<str>) -> Self {
36 S3Object {
37 bucket: bucket.as_ref().to_owned(),
38 key: key.as_ref().trim_start_matches('/').to_owned(),
39 }
40 }
41}
42
43impl TryFrom<Url> for S3Object {
46 type Error = S3ObjectError;
47
48 fn try_from(value: Url) -> Result<Self, Self::Error> {
49 if value.scheme() != "s3" {
50 return Err(S3ObjectError::UrlSchemeError);
51 }
52 let bucket = value.host_str().ok_or(S3ObjectError::UrlHostError)?;
53 let key = value.path();
54
55 if key.is_empty() {
56 return Err(S3ObjectError::UrlPathError);
57 }
58 Ok(S3Object::new(bucket, key))
59 }
60}
61
62impl TryFrom<&str> for S3Object {
65 type Error = S3ObjectError;
66
67 fn try_from(value: &str) -> Result<Self, Self::Error> {
68 value.parse::<Url>()?.try_into()
69 }
70}
71
72impl TryFrom<String> for S3Object {
75 type Error = S3ObjectError;
76
77 fn try_from(value: String) -> Result<Self, Self::Error> {
78 value.parse::<Url>()?.try_into()
79 }
80}
81
82impl FromStr for S3Object {
85 type Err = S3ObjectError;
86
87 fn from_str(value: &str) -> Result<Self, Self::Err> {
88 value.parse::<Url>()?.try_into()
89 }
90}
91
92impl TryFrom<S3Object> for Url {
96 type Error = url::ParseError;
97 fn try_from(obj: S3Object) -> std::result::Result<Self, Self::Error> {
98 Url::parse(&format!("s3://{}/{}", obj.bucket, obj.key))
99 }
100}
101
102impl TryFrom<&S3Object> for Url {
106 type Error = url::ParseError;
107 fn try_from(obj: &S3Object) -> std::result::Result<Self, Self::Error> {
108 Url::parse(&format!("s3://{}/{}", obj.bucket, obj.key))
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_s3_tryfrom() {
118 let bucket = "test-bucket.to_owned()";
119 let key = "test-key";
120 let url: url::Url = format!("s3://{bucket}/{key}")
121 .parse()
122 .expect("Expected successful URL parsing");
123 let obj: S3Object = url.try_into().expect("Expected successful URL conversion");
124 assert_eq!(bucket, obj.bucket);
125 assert_eq!(key, obj.key);
126 }
127
128 #[test]
129 fn test_s3_tryfrom_no_path() {
130 let url: url::Url = "s3://test-bucket"
131 .parse()
132 .expect("Expected successful URL parsing");
133 let result: Result<S3Object, S3ObjectError> = url.try_into();
134 assert!(result.is_err())
135 }
136
137 #[test]
138 fn test_s3_tryfrom_file_url() {
139 let url: url::Url = "file://path/to/file"
140 .parse()
141 .expect("Expected successful URL parsing");
142 let result: Result<S3Object, S3ObjectError> = url.try_into();
143 assert!(result.is_err())
144 }
145}