use std::sync::Arc; use pile_config::Label; use regex::Regex; use smartstring::{LazyCompact, SmartString}; use crate::{ extract::traits::{ExtractState, ListExtractor, ObjectExtractor}, value::PileValue, }; struct RegexData { regex: Arc, /// Captured substrings indexed by group index (0 = whole match). captures: Vec>>>, } impl RegexData { fn new(regex: Arc, input: &str) -> Option { let caps = regex.captures(input)?; let captures = caps .iter() .map(|m| m.map(|m| Arc::new(m.as_str().into()))) .collect(); Some(Self { regex, captures }) } } /// Exposes named capture groups as object fields. pub struct RegexExtractor(Arc); impl RegexExtractor { /// Run `regex` against `input`. Returns `None` if there is no match. pub fn new(regex: Arc, input: &str) -> Option { Some(Self(Arc::new(RegexData::new(regex, input)?))) } } #[async_trait::async_trait] impl ObjectExtractor for RegexExtractor { async fn field( &self, _state: &ExtractState, name: &Label, args: Option<&str>, ) -> Result, std::io::Error> { if args.is_some() { return Ok(None); } let Some(idx) = self .0 .regex .capture_names() .position(|n| n == Some(name.as_str())) else { return Ok(None); }; Ok(Some( match self.0.captures.get(idx).and_then(|v| v.as_ref()) { Some(s) => PileValue::String(s.clone()), None => PileValue::Null, }, )) } async fn fields(&self) -> Result, std::io::Error> { #[expect(clippy::unwrap_used)] Ok(self .0 .regex .capture_names() .flatten() .map(|n| Label::new(n).unwrap()) .collect()) } fn as_list(&self) -> Option> { Some(Arc::new(RegexExtractor(self.0.clone()))) } } #[async_trait::async_trait] impl ListExtractor for RegexExtractor { async fn get( &self, _state: &ExtractState, idx: usize, ) -> Result, std::io::Error> { let raw_idx = idx + 1; let Some(slot) = self.0.captures.get(raw_idx) else { return Ok(None); }; Ok(Some(match slot { Some(s) => PileValue::String(s.clone()), None => PileValue::Null, })) } async fn len(&self, _state: &ExtractState) -> Result { Ok(self.0.captures.len().saturating_sub(1)) } }