From 85a4bcdca1f34dbbede3ef4e81146c6c1ef049a5 Mon Sep 17 00:00:00 2001 From: Bilal Elmoussaoui Date: Tue, 31 Mar 2026 14:33:16 +0100 Subject: [PATCH] client: Add missing attributes_as in wrapper keyring --- client/src/keyring.rs | 34 +++++++++++ client/tests/keyring.rs | 131 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) diff --git a/client/src/keyring.rs b/client/src/keyring.rs index f459d471a..5b7f2f2e3 100644 --- a/client/src/keyring.rs +++ b/client/src/keyring.rs @@ -375,6 +375,40 @@ impl Item { Ok(attributes) } + /// Retrieve the item attributes as a typed schema. + /// + /// # Example + /// + /// ```no_run + /// # use oo7::{SecretSchema, Item}; + /// # #[derive(SecretSchema, Debug)] + /// # #[schema(name = "org.example.Password")] + /// # struct PasswordSchema { + /// # username: String, + /// # server: String, + /// # } + /// # async fn example(item: &Item) -> Result<(), oo7::Error> { + /// let schema = item.attributes_as::().await?; + /// println!("Username: {}", schema.username); + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "schema")] + #[cfg_attr(docsrs, doc(cfg(feature = "schema")))] + pub async fn attributes_as(&self) -> Result + where + T: for<'a> std::convert::TryFrom<&'a HashMap, Error = crate::SchemaError>, + { + match self { + Self::File(_, _) => T::try_from(&self.attributes().await?) + .map_err(crate::file::Error::Schema) + .map_err(Into::into), + Self::DBus(_) => T::try_from(&self.attributes().await?) + .map_err(crate::dbus::Error::Schema) + .map_err(Into::into), + } + } + /// Sets the item attributes. pub async fn set_attributes(&self, attributes: &impl AsAttributes) -> Result<()> { match self { diff --git a/client/tests/keyring.rs b/client/tests/keyring.rs index 98d92202d..4ee5e9288 100644 --- a/client/tests/keyring.rs +++ b/client/tests/keyring.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "schema")] +use oo7::{ContentType, SecretSchema}; use oo7::{Keyring, Secret, file}; use tempfile::tempdir; @@ -612,3 +614,132 @@ async fn item_lock_with_locked_keyring_fails() { keyring.delete(&[("app", "test")]).await.unwrap(); } } + +#[tokio::test] +#[cfg(all(feature = "tokio", feature = "schema"))] +async fn attributes_as() { + #[derive(SecretSchema, Debug, Default, PartialEq)] + #[schema(name = "org.example.Test")] + struct TestSchema { + username: String, + port: Option, + } + + let temp_dir = tempdir().unwrap(); + let (_setup, backends) = all_backends(&temp_dir).await; + + for (idx, keyring) in backends.iter().enumerate() { + println!("Testing attributes_as on backend {}", idx); + + // Create an item with text content + keyring + .create_item( + "Text Item", + &TestSchema { + username: "alice".to_string(), + port: Some(8080), + }, + Secret::text("my-password"), + true, + ) + .await + .unwrap(); + + // Create an item with blob content + keyring + .create_item( + "Blob Item", + &TestSchema { + username: "bob".to_string(), + port: None, + }, + Secret::blob(b"binary data"), + true, + ) + .await + .unwrap(); + + // Search for the text item + let text_items = keyring + .search_items(&TestSchema { + username: "alice".to_string(), + ..Default::default() + }) + .await + .unwrap(); + + assert_eq!(text_items.len(), 1); + let text_item = &text_items[0]; + + // Verify content type + let attrs = text_item.attributes().await.unwrap(); + assert_eq!(attrs.get("xdg:content-type").unwrap(), "text/plain"); + assert_eq!( + text_item.secret().await.unwrap().content_type(), + ContentType::Text + ); + + // Test attributes_as + let schema = text_item.attributes_as::().await.unwrap(); + assert_eq!( + schema, + TestSchema { + username: "alice".to_string(), + port: Some(8080) + } + ); + assert_eq!(schema.username, "alice"); + assert_eq!(schema.port, Some(8080)); + + // Search for the blob item + let blob_items = keyring + .search_items(&TestSchema { + username: "bob".to_string(), + ..Default::default() + }) + .await + .unwrap(); + + assert_eq!(blob_items.len(), 1); + let blob_item = &blob_items[0]; + + // Verify content type + let attrs = blob_item.attributes().await.unwrap(); + assert_eq!( + attrs.get("xdg:content-type").unwrap(), + "application/octet-stream" + ); + assert_eq!( + blob_item.secret().await.unwrap().content_type(), + ContentType::Blob + ); + + // Test attributes_as + let schema = blob_item.attributes_as::().await.unwrap(); + assert_eq!( + schema, + TestSchema { + username: "bob".to_string(), + port: None + } + ); + assert_eq!(schema.username, "bob"); + assert_eq!(schema.port, None); + + // Cleanup + keyring + .delete(&TestSchema { + username: "alice".to_string(), + ..Default::default() + }) + .await + .unwrap(); + keyring + .delete(&TestSchema { + username: "bob".to_string(), + ..Default::default() + }) + .await + .unwrap(); + } +}