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>, } impl StringExtractor { pub fn new(item: &Arc>) -> 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, 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, 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 { let state = ExtractState { ignore_mime: false }; ext.field(&state, &Label::new(name).unwrap(), args) .await .unwrap() } fn string(v: Option) -> Option { match v? { PileValue::String(s) => Some(s.as_str().to_owned()), _ => panic!("expected string"), } } fn array(v: Option) -> Vec { 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()); } }