/// An attachment that extracts metadata from an [Item]. /// /// Metadata is exposed as an immutable map of {label: value}, /// much like a json object. #[async_trait::async_trait] pub trait ObjectExtractor: Send + Sync { /// Get the field at `name` from `item`. /// - returns `None` if `name` is not a valid field /// - returns `Some(Null)` if `name` is not available async fn field( &self, name: &pile_config::Label, ) -> Result, std::io::Error>; /// Return all fields in this extractor. /// `Self::field` must return [Some] for all these keys /// and [None] for all others. async fn fields(&self) -> Result, std::io::Error>; /// Convert this to a JSON value. async fn to_json(&self) -> Result { let keys = self.fields().await?; let mut map = serde_json::Map::new(); for k in &keys { let v = match self.field(k).await? { Some(x) => x, None => continue, }; map.insert(k.to_string(), Box::pin(v.to_json()).await?); } Ok(serde_json::Value::Object(map)) } } /// An attachment that extracts metadata from an [Item]. /// /// Metadata is exposed as an immutable list of values. #[async_trait::async_trait] pub trait ListExtractor: Send + Sync { /// Get the item at index `idx`. /// Indices start at zero, and must be consecutive. /// - returns `None` if `idx` is out of range /// - returns `Some(Null)` if `None` is at `idx` async fn get(&self, idx: usize) -> Result, std::io::Error>; async fn len(&self) -> Result; async fn is_empty(&self) -> Result { Ok(self.len().await? == 0) } /// Convert this list to a JSON value. async fn to_json(&self) -> Result { let len = self.len().await?; let mut list = Vec::with_capacity(len); for i in 0..len { #[expect(clippy::expect_used)] let v = self .get(i) .await? .expect("value must be present according to length"); list.push(Box::pin(v.to_json()).await?); } Ok(serde_json::Value::Array(list)) } }