Skip to content

Commit b49dd85

Browse files
committed
crate::path::Path improvements
1 parent 0661843 commit b49dd85

File tree

2 files changed

+111
-14
lines changed

2 files changed

+111
-14
lines changed

src/path/mod.rs

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
//! Path abstraction for Object Storage
1919
20-
use itertools::Itertools;
2120
use percent_encoding::percent_decode;
2221
use std::fmt::Formatter;
2322
#[cfg(not(target_arch = "wasm32"))]
@@ -29,9 +28,12 @@ pub const DELIMITER: &str = "/";
2928
/// The path delimiter as a single byte
3029
pub const DELIMITER_BYTE: u8 = DELIMITER.as_bytes()[0];
3130

31+
/// The path delimiter as a single char
32+
pub const DELIMITER_CHAR: char = DELIMITER_BYTE as char;
33+
3234
mod parts;
3335

34-
pub use parts::{InvalidPart, PathPart};
36+
pub use parts::{InvalidPart, PathPart, PathParts};
3537

3638
/// Error returned by [`Path::parse`]
3739
#[derive(Debug, thiserror::Error)]
@@ -157,6 +159,18 @@ pub struct Path {
157159
}
158160

159161
impl Path {
162+
/// An empty [`Path`] that points to the root of the store, equivalent to `Path::from("/")`.
163+
///
164+
/// See also [`Path::is_root`].
165+
///
166+
/// # Example
167+
///
168+
/// ```
169+
/// # use object_store::path::Path;
170+
/// assert_eq!(Path::ROOT, Path::from("/"));
171+
/// ```
172+
pub const ROOT: Self = Self { raw: String::new() };
173+
160174
/// Parse a string as a [`Path`], returning a [`Error`] if invalid,
161175
/// as defined on the docstring for [`Path`]
162176
///
@@ -255,14 +269,53 @@ impl Path {
255269
Self::parse(decoded)
256270
}
257271

258-
/// Returns the [`PathPart`] of this [`Path`]
259-
pub fn parts(&self) -> impl Iterator<Item = PathPart<'_>> {
260-
self.raw
261-
.split_terminator(DELIMITER)
262-
.map(|s| PathPart { raw: s.into() })
272+
/// Returns the number of [`PathPart`]s in this [`Path`]
273+
///
274+
/// This is equivalent to calling `.parts().count()` manually.
275+
///
276+
/// # Performance
277+
///
278+
/// This operation is `O(n)`.
279+
#[doc(alias = "len")]
280+
pub fn parts_count(&self) -> usize {
281+
self.raw.split_terminator(DELIMITER).count()
282+
}
283+
284+
/// True if this [`Path`] points to the root of the store, equivalent to `Path::from("/")`.
285+
///
286+
/// See also [`Path::root`].
287+
///
288+
/// # Example
289+
///
290+
/// ```
291+
/// # use object_store::path::Path;
292+
/// assert!(Path::from("/").is_root());
293+
/// assert!(Path::parse("").unwrap().is_root());
294+
/// ```
295+
pub fn is_root(&self) -> bool {
296+
self.raw.is_empty()
297+
}
298+
299+
/// Returns the [`PathPart`]s of this [`Path`]
300+
pub fn parts(&self) -> PathParts<'_> {
301+
PathParts::new(self)
302+
}
303+
304+
/// Returns a copy of this [`Path`] with the last path segment removed
305+
///
306+
/// Returns `None` if this path has zero segments.
307+
#[doc(alias = "folder")]
308+
pub fn prefix(&self) -> Option<Self> {
309+
let prefix = self.raw.rsplit_once(DELIMITER)?.0;
310+
311+
Some(Self {
312+
raw: prefix.to_string(),
313+
})
263314
}
264315

265316
/// Returns the last path segment containing the filename stored in this [`Path`]
317+
///
318+
/// Returns `None` only if this path is the root path.
266319
pub fn filename(&self) -> Option<&str> {
267320
match self.raw.is_empty() {
268321
true => None,
@@ -348,13 +401,22 @@ where
348401
I: Into<PathPart<'a>>,
349402
{
350403
fn from_iter<T: IntoIterator<Item = I>>(iter: T) -> Self {
351-
let raw = T::into_iter(iter)
352-
.map(|s| s.into())
353-
.filter(|s| !s.raw.is_empty())
354-
.map(|s| s.raw)
355-
.join(DELIMITER);
404+
let mut this = Self::ROOT;
405+
this.extend(iter);
406+
this
407+
}
408+
}
356409

357-
Self { raw }
410+
impl<'a, I: Into<PathPart<'a>>> Extend<I> for Path {
411+
fn extend<T: IntoIterator<Item = I>>(&mut self, iter: T) {
412+
for s in iter.into_iter() {
413+
let s = s.into();
414+
if s.raw.is_empty() {
415+
continue;
416+
}
417+
self.raw.push(DELIMITER_CHAR);
418+
self.raw.push_str(&s.raw);
419+
}
358420
}
359421
}
360422

src/path/parts.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
// under the License.
1717

1818
use percent_encoding::{AsciiSet, CONTROLS, percent_encode};
19-
use std::borrow::Cow;
19+
use std::{
20+
borrow::Cow,
21+
iter::{self, FusedIterator},
22+
str::SplitTerminator,
23+
};
2024

2125
use crate::path::DELIMITER_BYTE;
2226

@@ -131,6 +135,37 @@ impl AsRef<str> for PathPart<'_> {
131135
}
132136
}
133137

138+
/// See [`Path::parts`](super::Path::parts)
139+
#[derive(Debug, Clone)]
140+
pub struct PathParts<'a>(iter::Map<SplitTerminator<'a, char>, fn(&str) -> PathPart<'_>>);
141+
142+
impl<'a> PathParts<'a> {
143+
/// Create an iterator over the parts of the provided [`Path`]
144+
pub(super) fn new(path: &'a super::Path) -> Self {
145+
Self(
146+
path.raw
147+
.split_terminator(super::DELIMITER_CHAR)
148+
.map(|s| PathPart { raw: s.into() }),
149+
)
150+
}
151+
}
152+
153+
impl<'a> Iterator for PathParts<'a> {
154+
type Item = PathPart<'a>;
155+
156+
fn next(&mut self) -> Option<Self::Item> {
157+
self.0.next()
158+
}
159+
}
160+
161+
impl<'a> FusedIterator for PathParts<'a> {}
162+
163+
impl<'a> DoubleEndedIterator for PathParts<'a> {
164+
fn next_back(&mut self) -> Option<Self::Item> {
165+
self.0.next_back()
166+
}
167+
}
168+
134169
#[cfg(test)]
135170
mod tests {
136171
use super::*;

0 commit comments

Comments
 (0)