use pile_config::Label; use smartstring::{LazyCompact, SmartString}; use std::sync::Arc; use crate::{extract::traits::ObjectExtractor, value::PileValue}; fn parse_name(s: &str) -> (&str, Option<&str>) { match s.find('(') { None => (s, None), Some(i) => { let name = &s[..i]; let rest = &s[i + 1..]; match rest.strip_suffix(')') { Some(args) => (name, Some(args)), None => (name, None), } } } } 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, name: &Label) -> Result, std::io::Error> { let (name, args) = parse_name(name.as_str()); Ok(match (name, 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)] 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) -> Option { ext.field(&Label::new(name).unwrap()).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").await), Some("hi".into()) ); } #[tokio::test] async fn trim_no_args() { assert!(field(&extractor("x"), "trim(foo)").await.is_none()); } #[tokio::test] async fn nonempty_with_content() { assert!(matches!( field(&extractor("hello"), "nonempty").await, Some(PileValue::String(_)) )); } #[tokio::test] async fn nonempty_empty_string() { assert!(matches!( field(&extractor(""), "nonempty").await, Some(PileValue::Null) )); } #[tokio::test] async fn trimprefix_present() { assert_eq!( string(field(&extractor("foobar"), "trimprefix(foo)").await), Some("bar".into()) ); } #[tokio::test] async fn trimprefix_absent() { assert_eq!( string(field(&extractor("foobar"), "trimprefix(baz)").await), Some("foobar".into()) ); } #[tokio::test] async fn trimprefix_no_args() { assert!(field(&extractor("foobar"), "trimprefix").await.is_none()); } #[tokio::test] async fn trimsuffix_present() { assert_eq!( string(field(&extractor("foobar"), "trimsuffix(bar)").await), Some("foo".into()) ); } #[tokio::test] async fn trimsuffix_absent() { assert_eq!( string(field(&extractor("foobar"), "trimsuffix(baz)").await), Some("foobar".into()) ); } #[tokio::test] async fn split_basic() { assert_eq!( array(field(&extractor("a,b,c"), "split(,)").await), vec!["a", "b", "c"] ); } #[tokio::test] async fn split_no_match() { assert_eq!( array(field(&extractor("abc"), "split(,)").await), vec!["abc"] ); } #[tokio::test] async fn split_no_args() { assert!(field(&extractor("abc"), "split").await.is_none()); } #[tokio::test] async fn split_unclosed_paren() { assert!(field(&extractor("abc"), "split(,").await.is_none()); } #[tokio::test] async fn unknown_field() { assert!(field(&extractor("abc"), "bogus").await.is_none()); } }