1pub mod error;
2mod validation;
3
4use crate::error::InvalidS3PathComponent;
5use std::borrow::Cow;
6use std::fmt::Formatter;
7use std::marker::PhantomData;
8use std::path::PathBuf;
9
10pub type S3PathComp<'i> = Cow<'i, str>;
11
12pub trait S3PathIter<'i, C>: IntoIterator<Item = C> + Clone {}
13
14impl<'i, C: Into<S3PathComp<'i>>, II: IntoIterator<Item = C> + Clone> S3PathIter<'i, C> for II {}
15
16#[derive(Clone)]
18pub struct S3Path<'i, C: Into<S3PathComp<'i>>, I: S3PathIter<'i, C>> {
19 components: I,
20 phantom_data: PhantomData<&'i ()>,
21 phantom_data2: PhantomData<C>,
22}
23
24impl<'i, C: Into<S3PathComp<'i>>, I: S3PathIter<'i, C>> S3Path<'i, C, I> {
25 pub fn try_from(components: I) -> Result<Self, InvalidS3PathComponent> {
26 for component in components.clone().into_iter() {
27 validation::validate_component(&component.into())?;
28 }
29 Ok(Self {
30 components,
31 phantom_data: PhantomData,
32 phantom_data2: PhantomData,
33 })
34 }
35
36 fn new_unchecked(components: I) -> Self {
37 Self {
38 components,
39 phantom_data: PhantomData,
40 phantom_data2: PhantomData,
41 }
42 }
43}
44
45impl<'i, C: Into<S3PathComp<'i>>, I: S3PathIter<'i, C>> std::fmt::Display for S3Path<'i, C, I> {
46 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
47 write_components(self.components.clone().into_iter().map(|it| it.into()), f)?;
48 Ok(())
49 }
50}
51
52impl<'i, C: Into<S3PathComp<'i>>, I: S3PathIter<'i, C>> std::fmt::Debug for S3Path<'i, C, I> {
53 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54 write_components(self.components.clone().into_iter().map(|it| it.into()), f)?;
55 Ok(())
56 }
57}
58
59#[derive(Clone, PartialEq, Eq, Default)]
61pub struct S3PathBuf {
62 components: Vec<Cow<'static, str>>,
63}
64
65impl S3PathBuf {
66 pub fn new() -> Self {
67 Self::default()
68 }
69
70 pub fn join(
71 &mut self,
72 component: impl Into<Cow<'static, str>>,
73 ) -> Result<&mut Self, InvalidS3PathComponent> {
74 let component_str = component.into();
75 validation::validate_component(component_str.as_ref())?;
76 self.components.push(component_str);
77 Ok(self)
78 }
79
80 pub fn as_path(&self) -> S3Path<Cow<str>, impl S3PathIter<Cow<str>>> {
81 S3Path::new_unchecked(self.components.iter().map(|it| Cow::Borrowed(it.as_ref())))
84 }
85
86 pub fn to_std_path_buf(&self) -> PathBuf {
87 let mut path = PathBuf::new();
88 for c in &self.components {
89 path = path.join(c.as_ref());
90 }
91 path
92 }
93}
94
95impl TryFrom<&str> for S3PathBuf {
96 type Error = InvalidS3PathComponent;
97
98 fn try_from(value: &str) -> Result<Self, Self::Error> {
99 let mut path = S3PathBuf::new();
100 for c in value.split('/') {
101 if !c.is_empty() {
103 path.join(Cow::Owned(c.to_string()))?;
104 }
105 }
106 Ok(path)
107 }
108}
109
110impl TryFrom<String> for S3PathBuf {
111 type Error = InvalidS3PathComponent;
112
113 fn try_from(value: String) -> Result<Self, Self::Error> {
114 S3PathBuf::try_from(value.as_str())
115 }
116}
117
118impl std::fmt::Display for S3PathBuf {
119 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
120 write_components(self.components.iter(), f)?;
121 Ok(())
122 }
123}
124
125impl std::fmt::Debug for S3PathBuf {
126 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127 write_components(self.components.iter(), f)?;
128 Ok(())
129 }
130}
131
132fn write_components<C: AsRef<str>>(
133 components: impl Iterator<Item = C>,
134 f: &mut Formatter,
135) -> Result<(), std::fmt::Error> {
136 for (i, c) in components.enumerate() {
137 if i > 0 {
138 f.write_str("/")?;
139 }
140 f.write_str(c.as_ref())?;
141 }
142 Ok(())
143}
144
145#[cfg(test)]
146mod test {
147 mod s3_path_buf {
148 use crate::S3PathBuf;
149 use assertr::prelude::*;
150
151 #[test]
152 fn new_is_initially_empty() {
153 let path = S3PathBuf::new();
154 assert_that(path).has_display_value("");
155 }
156
157 #[test]
158 fn construct_using_new_and_join_components() {
159 let mut path = S3PathBuf::new();
160 path.join("foo").unwrap();
161 path.join("bar").unwrap();
162 assert_that(path).has_display_value("foo/bar");
163 }
164
165 #[test]
166 fn construct_using_try_from_given_str() {
167 let path = S3PathBuf::try_from("foo/bar").unwrap();
168 assert_that(path).has_display_value("foo/bar");
169 }
170
171 #[test]
172 fn construct_using_try_from_given_string() {
173 let path = S3PathBuf::try_from("foo/bar".to_string()).unwrap();
174 assert_that(path).has_display_value("foo/bar");
175 }
176
177 #[test]
178 fn reject_invalid_characters() {
179 let mut path = S3PathBuf::new();
180 let result = path.join("invalid/path");
181 assert_that(result.is_err()).is_true();
182
183 let result = S3PathBuf::try_from("foo/bar$baz");
184 assert_that(result.is_err()).is_true();
185 }
186 }
187
188 mod s3_path {
189
190 mod new {
191 use crate::S3Path;
192 use assertr::prelude::*;
193
194 #[test]
195 fn from_static_str_array() {
196 let path = S3Path::try_from(["foo", "bar"]).unwrap();
197 assert_that(path).has_display_value("foo/bar");
198 }
199
200 #[test]
201 fn from_borrowed_str_array() {
202 let components = ["foo".to_string(), "bar".to_string()];
203 let components_ref = [components[0].as_str(), components[1].as_str()];
204 let path = S3Path::try_from(components_ref).unwrap();
205 assert_that(path).has_display_value("foo/bar");
206 }
207
208 #[test]
209 fn from_string_array() {
210 let path = S3Path::try_from(["foo".to_string(), "bar".to_string()]).unwrap();
211 assert_that(path).has_display_value("foo/bar");
212 }
213
214 #[test]
215 fn reject_invalid_characters() {
216 assert_that(S3Path::try_from(["foo-bar"]))
217 .is_ok()
218 .has_display_value("foo-bar");
219 assert_that(S3Path::try_from(["foo_bar"]))
220 .is_ok()
221 .has_display_value("foo_bar");
222 assert_that(S3Path::try_from(["foo.bar"]))
223 .is_ok()
224 .has_display_value("foo.bar");
225 assert_that(S3Path::try_from([".test"]))
226 .is_ok()
227 .has_display_value(".test");
228
229 assert_that(S3Path::try_from(["foo$bar"])).is_err();
230 assert_that(S3Path::try_from(["foo&bar"])).is_err();
231 assert_that(S3Path::try_from(["foo#bar"])).is_err();
232 assert_that(S3Path::try_from(["foo/bar"])).is_err();
233 assert_that(S3Path::try_from(["foo|bar"])).is_err();
234 assert_that(S3Path::try_from(["foo\\bar"])).is_err();
235 assert_that(S3Path::try_from(["."])).is_err();
236 assert_that(S3Path::try_from([".."])).is_err();
237 }
238 }
239 }
240}