Files
pile/crates/pile-value/src/extract/string.rs
rm-dr a2079877fd
Some checks failed
CI / Typos (push) Successful in 17s
CI / Build and test (push) Successful in 2m28s
CI / Clippy (push) Failing after 3m59s
CI / Build and test (all features) (push) Successful in 9m40s
Filter by mime
2026-03-15 10:20:15 -07:00

218 lines
4.6 KiB
Rust

use pile_config::Label;
use smartstring::{LazyCompact, SmartString};
use std::sync::Arc;
use crate::{
extract::traits::{ExtractState, ObjectExtractor},
value::PileValue,
};
pub struct StringExtractor {
item: Arc<SmartString<LazyCompact>>,
}
impl StringExtractor {
pub fn new(item: &Arc<SmartString<LazyCompact>>) -> Self {
Self { item: item.clone() }
}
}
#[async_trait::async_trait]
impl ObjectExtractor for StringExtractor {
async fn field(
&self,
_state: &ExtractState,
name: &Label,
args: Option<&str>,
) -> Result<Option<PileValue>, std::io::Error> {
Ok(match (name.as_str(), args) {
("trim", None) => Some(PileValue::String(Arc::new(
self.item.as_str().trim().into(),
))),
("upper", None) => Some(PileValue::String(Arc::new(
self.item.as_str().to_lowercase().into(),
))),
("lower", None) => Some(PileValue::String(Arc::new(
self.item.as_str().to_uppercase().into(),
))),
("nonempty", None) => Some(match self.item.is_empty() {
true => PileValue::Null,
false => PileValue::String(self.item.clone()),
}),
("trimprefix", Some(prefix)) => Some(PileValue::String(Arc::new(
self.item
.as_str()
.strip_prefix(prefix)
.unwrap_or(self.item.as_str())
.into(),
))),
("trimsuffix", Some(suffix)) => Some(PileValue::String(Arc::new(
self.item
.as_str()
.strip_suffix(suffix)
.unwrap_or(self.item.as_str())
.into(),
))),
("split", Some(by)) => Some(PileValue::Array(Arc::new(
self.item
.as_str()
.split(by)
.map(|s| PileValue::String(Arc::new(s.into())))
.collect(),
))),
_ => None,
})
}
#[expect(clippy::unwrap_used)]
async fn fields(&self) -> Result<Vec<Label>, std::io::Error> {
return Ok(vec![
Label::new("trim").unwrap(),
Label::new("upper").unwrap(),
Label::new("lower").unwrap(),
Label::new("nonempty").unwrap(),
]);
}
}
#[cfg(test)]
#[expect(clippy::expect_used)]
mod tests {
use super::*;
fn extractor(s: &str) -> StringExtractor {
StringExtractor::new(&Arc::new(s.into()))
}
#[expect(clippy::unwrap_used)]
async fn field(ext: &StringExtractor, name: &str, args: Option<&str>) -> Option<PileValue> {
let state = ExtractState { ignore_mime: false };
ext.field(&state, &Label::new(name).unwrap(), args)
.await
.unwrap()
}
fn string(v: Option<PileValue>) -> Option<String> {
match v? {
PileValue::String(s) => Some(s.as_str().to_owned()),
_ => panic!("expected string"),
}
}
fn array(v: Option<PileValue>) -> Vec<String> {
match v.expect("expected Some") {
PileValue::Array(arr) => arr
.iter()
.map(|v| match v {
PileValue::String(s) => s.as_str().to_owned(),
_ => panic!("expected string element"),
})
.collect(),
_ => panic!("expected array"),
}
}
#[tokio::test]
async fn trim() {
assert_eq!(
string(field(&extractor(" hi "), "trim", None).await),
Some("hi".into())
);
}
#[tokio::test]
async fn trim_no_args() {
assert!(field(&extractor("x"), "trim", Some("foo")).await.is_none());
}
#[tokio::test]
async fn nonempty_with_content() {
assert!(matches!(
field(&extractor("hello"), "nonempty", None).await,
Some(PileValue::String(_))
));
}
#[tokio::test]
async fn nonempty_empty_string() {
assert!(matches!(
field(&extractor(""), "nonempty", None).await,
Some(PileValue::Null)
));
}
#[tokio::test]
async fn trimprefix_present() {
assert_eq!(
string(field(&extractor("foobar"), "trimprefix", Some("foo")).await),
Some("bar".into())
);
}
#[tokio::test]
async fn trimprefix_absent() {
assert_eq!(
string(field(&extractor("foobar"), "trimprefix", Some("baz")).await),
Some("foobar".into())
);
}
#[tokio::test]
async fn trimprefix_no_args() {
assert!(
field(&extractor("foobar"), "trimprefix", None)
.await
.is_none()
);
}
#[tokio::test]
async fn trimsuffix_present() {
assert_eq!(
string(field(&extractor("foobar"), "trimsuffix", Some("bar")).await),
Some("foo".into())
);
}
#[tokio::test]
async fn trimsuffix_absent() {
assert_eq!(
string(field(&extractor("foobar"), "trimsuffix", Some("baz")).await),
Some("foobar".into())
);
}
#[tokio::test]
async fn split_basic() {
assert_eq!(
array(field(&extractor("a,b,c"), "split", Some(",")).await),
vec!["a", "b", "c"]
);
}
#[tokio::test]
async fn split_no_match() {
assert_eq!(
array(field(&extractor("abc"), "split", Some(",")).await),
vec!["abc"]
);
}
#[tokio::test]
async fn split_no_args() {
assert!(field(&extractor("abc"), "split", None).await.is_none());
}
#[tokio::test]
async fn unknown_field() {
assert!(field(&extractor("abc"), "bogus", None).await.is_none());
}
}