pub mod error;
mod validation;
use crate::error::InvalidS3PathComponent;
use std::borrow::Cow;
use std::fmt::Formatter;
use std::marker::PhantomData;
use std::path::PathBuf;
pub type S3PathComp<'i> = Cow<'i, str>;
pub trait S3PathIter<'i, C>: IntoIterator<Item = C> + Clone {}
impl<'i, C: Into<S3PathComp<'i>>, II: IntoIterator<Item = C> + Clone> S3PathIter<'i, C> for II {}
#[derive(Clone)]
pub struct S3Path<'i, C: Into<S3PathComp<'i>>, I: S3PathIter<'i, C>> {
components: I,
phantom_data: PhantomData<&'i ()>,
phantom_data2: PhantomData<C>,
}
impl<'i, C: Into<S3PathComp<'i>>, I: S3PathIter<'i, C>> S3Path<'i, C, I> {
pub fn try_from(components: I) -> Result<Self, InvalidS3PathComponent> {
for component in components.clone().into_iter() {
validation::validate_component(&component.into())?;
}
Ok(Self {
components,
phantom_data: PhantomData,
phantom_data2: PhantomData,
})
}
fn new_unchecked(components: I) -> Self {
Self {
components,
phantom_data: PhantomData,
phantom_data2: PhantomData,
}
}
}
impl<'i, C: Into<S3PathComp<'i>>, I: S3PathIter<'i, C>> std::fmt::Display for S3Path<'i, C, I> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write_components(self.components.clone().into_iter().map(|it| it.into()), f)?;
Ok(())
}
}
impl<'i, C: Into<S3PathComp<'i>>, I: S3PathIter<'i, C>> std::fmt::Debug for S3Path<'i, C, I> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write_components(self.components.clone().into_iter().map(|it| it.into()), f)?;
Ok(())
}
}
#[derive(Clone, PartialEq, Eq, Default)]
pub struct S3PathBuf {
components: Vec<Cow<'static, str>>,
}
impl S3PathBuf {
pub fn new() -> Self {
Self::default()
}
pub fn join(
&mut self,
component: impl Into<Cow<'static, str>>,
) -> Result<&mut Self, InvalidS3PathComponent> {
let component_str = component.into();
validation::validate_component(component_str.as_ref())?;
self.components.push(component_str);
Ok(self)
}
pub fn as_path(&self) -> S3Path<Cow<str>, impl S3PathIter<Cow<str>>> {
S3Path::new_unchecked(self.components.iter().map(|it| Cow::Borrowed(it.as_ref())))
}
pub fn to_std_path_buf(&self) -> PathBuf {
let mut path = PathBuf::new();
for c in &self.components {
path = path.join(c.as_ref());
}
path
}
}
impl TryFrom<&str> for S3PathBuf {
type Error = InvalidS3PathComponent;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut path = S3PathBuf::new();
for c in value.split('/') {
if !c.is_empty() {
path.join(Cow::Owned(c.to_string()))?;
}
}
Ok(path)
}
}
impl TryFrom<String> for S3PathBuf {
type Error = InvalidS3PathComponent;
fn try_from(value: String) -> Result<Self, Self::Error> {
S3PathBuf::try_from(value.as_str())
}
}
impl std::fmt::Display for S3PathBuf {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write_components(self.components.iter(), f)?;
Ok(())
}
}
impl std::fmt::Debug for S3PathBuf {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write_components(self.components.iter(), f)?;
Ok(())
}
}
fn write_components<C: AsRef<str>>(
components: impl Iterator<Item = C>,
f: &mut Formatter,
) -> Result<(), std::fmt::Error> {
for (i, c) in components.enumerate() {
if i > 0 {
f.write_str("/")?;
}
f.write_str(c.as_ref())?;
}
Ok(())
}
#[cfg(test)]
mod test {
mod s3_path_buf {
use crate::S3PathBuf;
use assertr::prelude::*;
#[test]
fn new_is_initially_empty() {
let path = S3PathBuf::new();
assert_that(path).has_display_value("");
}
#[test]
fn construct_using_new_and_join_components() {
let mut path = S3PathBuf::new();
path.join("foo").unwrap();
path.join("bar").unwrap();
assert_that(path).has_display_value("foo/bar");
}
#[test]
fn construct_using_try_from_given_str() {
let path = S3PathBuf::try_from("foo/bar").unwrap();
assert_that(path).has_display_value("foo/bar");
}
#[test]
fn construct_using_try_from_given_string() {
let path = S3PathBuf::try_from("foo/bar".to_string()).unwrap();
assert_that(path).has_display_value("foo/bar");
}
#[test]
fn reject_invalid_characters() {
let mut path = S3PathBuf::new();
let result = path.join("invalid/path");
assert_that(result.is_err()).is_true();
let result = S3PathBuf::try_from("foo/bar$baz");
assert_that(result.is_err()).is_true();
}
}
mod s3_path {
mod new {
use crate::S3Path;
use assertr::prelude::*;
#[test]
fn from_static_str_array() {
let path = S3Path::try_from(["foo", "bar"]).unwrap();
assert_that(path).has_display_value("foo/bar");
}
#[test]
fn from_borrowed_str_array() {
let components = ["foo".to_string(), "bar".to_string()];
let components_ref = [components[0].as_str(), components[1].as_str()];
let path = S3Path::try_from(components_ref).unwrap();
assert_that(path).has_display_value("foo/bar");
}
#[test]
fn from_string_array() {
let path = S3Path::try_from(["foo".to_string(), "bar".to_string()]).unwrap();
assert_that(path).has_display_value("foo/bar");
}
#[test]
fn reject_invalid_characters() {
assert_that(S3Path::try_from(["foo-bar"]))
.is_ok()
.has_display_value("foo-bar");
assert_that(S3Path::try_from(["foo_bar"]))
.is_ok()
.has_display_value("foo_bar");
assert_that(S3Path::try_from(["foo.bar"]))
.is_ok()
.has_display_value("foo.bar");
assert_that(S3Path::try_from([".test"]))
.is_ok()
.has_display_value(".test");
assert_that(S3Path::try_from(["foo$bar"])).is_err();
assert_that(S3Path::try_from(["foo&bar"])).is_err();
assert_that(S3Path::try_from(["foo#bar"])).is_err();
assert_that(S3Path::try_from(["foo/bar"])).is_err();
assert_that(S3Path::try_from(["foo|bar"])).is_err();
assert_that(S3Path::try_from(["foo\\bar"])).is_err();
assert_that(S3Path::try_from(["."])).is_err();
assert_that(S3Path::try_from([".."])).is_err();
}
}
}
}