use crate::client::AnyListClient;
use crate::error::{AnyListError, Result};
use crate::protobuf::anylist::{
PbEmailUserIdPair, PbListItem, PbShoppingListsResponse, PbUserDataResponse,
};
use crate::utils::{current_timestamp, generate_id};
use prost::Message;
use serde_derive::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserInfo {
pub(crate) user_id: String,
pub(crate) email: Option<String>,
pub(crate) full_name: Option<String>,
}
impl UserInfo {
pub fn user_id(&self) -> &str {
&self.user_id
}
pub fn email(&self) -> Option<&str> {
self.email.as_deref()
}
pub fn full_name(&self) -> Option<&str> {
self.full_name.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListItem {
pub(crate) id: String,
pub(crate) list_id: String,
pub(crate) name: String,
pub(crate) details: String,
pub(crate) is_checked: bool,
pub(crate) quantity: Option<String>,
pub(crate) category: Option<String>,
pub(crate) user_id: Option<String>,
pub(crate) product_upc: Option<String>,
}
impl ListItem {
pub fn id(&self) -> &str {
&self.id
}
pub fn list_id(&self) -> &str {
&self.list_id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn details(&self) -> &str {
&self.details
}
pub fn is_checked(&self) -> bool {
self.is_checked
}
pub fn quantity(&self) -> Option<&str> {
self.quantity.as_deref()
}
pub fn category(&self) -> Option<&str> {
self.category.as_deref()
}
pub fn user_id(&self) -> Option<&str> {
self.user_id.as_deref()
}
pub fn product_upc(&self) -> Option<&str> {
self.product_upc.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct List {
pub(crate) id: String,
pub(crate) name: String,
pub(crate) items: Vec<ListItem>,
pub(crate) shared_users: Vec<UserInfo>,
}
impl List {
pub fn id(&self) -> &str {
&self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn items(&self) -> &[ListItem] {
&self.items
}
pub fn shared_users(&self) -> &[UserInfo] {
&self.shared_users
}
}
impl AnyListClient {
pub async fn get_lists(&self) -> Result<Vec<List>> {
let data = self.get_user_data().await?;
let lists = match data.shopping_lists_response {
Some(ref res) => lists_from_response(res.clone()),
None => Vec::new(),
};
Ok(lists)
}
pub async fn get_list_by_id(&self, list_id: &str) -> Result<List> {
let lists = self.get_lists().await?;
lists
.into_iter()
.find(|l| l.id == list_id)
.ok_or_else(|| AnyListError::NotFound(format!("List with ID {} not found", list_id)))
}
pub async fn get_list_by_name(&self, name: &str) -> Result<List> {
let lists = self.get_lists().await?;
lists
.into_iter()
.find(|l| l.name == name)
.ok_or_else(|| AnyListError::NotFound(format!("List with name '{}' not found", name)))
}
pub async fn create_list(&self, name: &str) -> Result<List> {
let list_id = generate_id();
let operation_id = generate_id();
let params = crate::operations::CreateListParams {
list_id: list_id.clone(),
operation_id,
user_id: self.user_id(),
timestamp: current_timestamp(),
name: name.to_string(),
};
let operation_list = crate::operations::build_create_list_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).map_err(|e| {
AnyListError::ProtobufError(format!("Failed to encode operation: {}", e))
})?;
self.post("data/shopping-lists/update", buf).await?;
Ok(List {
id: list_id,
name: name.to_string(),
items: vec![],
shared_users: vec![],
})
}
pub async fn delete_list(&self, list_id: &str) -> Result<()> {
let user_data = self.get_user_data().await?;
let list_data_id = user_data
.list_folders_response
.as_ref()
.and_then(|r| r.list_data_id.clone())
.ok_or_else(|| {
AnyListError::NotFound("Could not find list_data_id for folder operations".into())
})?;
let settings_id = user_data
.list_settings_response
.as_ref()
.and_then(|r| {
r.settings
.iter()
.find(|s| s.list_id.as_deref() == Some(list_id))
.map(|s| s.identifier.clone())
})
.ok_or_else(|| {
AnyListError::NotFound(format!("Could not find settings for list {}", list_id))
})?;
let folder_params = crate::operations::DeleteFolderItemsParams {
list_id: list_id.to_string(),
list_data_id,
operation_id: generate_id(),
user_id: self.user_id(),
};
let folder_operation_list = crate::operations::build_delete_folder_items_operation(folder_params);
let mut folder_buf = Vec::new();
folder_operation_list.encode(&mut folder_buf).map_err(|e| {
AnyListError::ProtobufError(format!("Failed to encode folder operation: {}", e))
})?;
self.post("data/list-folders/update", folder_buf).await?;
let settings_params = crate::operations::RemoveListSettingsParams {
settings_id,
list_id: list_id.to_string(),
operation_id: generate_id(),
user_id: self.user_id(),
};
let settings_operation_list = crate::operations::build_remove_list_settings_operation(settings_params);
let mut settings_buf = Vec::new();
settings_operation_list.encode(&mut settings_buf).map_err(|e| {
AnyListError::ProtobufError(format!("Failed to encode settings operation: {}", e))
})?;
self.post("data/list-settings/update", settings_buf).await?;
Ok(())
}
pub async fn rename_list(&self, list_id: &str, new_name: &str) -> Result<()> {
let operation_id = generate_id();
let current_list = self.get_list_by_id(list_id).await?;
let params = crate::operations::RenameListParams {
list_id: list_id.to_string(),
operation_id,
user_id: self.user_id(),
timestamp: current_timestamp(),
old_name: current_list.name,
new_name: new_name.to_string(),
};
let operation_list = crate::operations::build_rename_list_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).map_err(|e| {
AnyListError::ProtobufError(format!("Failed to encode operation: {}", e))
})?;
self.post("data/shopping-lists/update", buf).await?;
Ok(())
}
pub async fn get_user_data(&self) -> Result<PbUserDataResponse> {
let bytes = self.post("data/user-data/get", vec![]).await?;
let data = PbUserDataResponse::decode(bytes.as_ref())?;
Ok(data)
}
}
fn transform_api_list_item(items: Vec<PbListItem>) -> Vec<ListItem> {
let mut result: Vec<ListItem> = Vec::new();
for item in items {
if let (Some(name), Some(list_id)) = (item.name, item.list_id) {
let item = ListItem {
id: item.identifier,
list_id: list_id.clone(),
name,
details: item.details.unwrap_or("".to_string()),
is_checked: item.checked.unwrap_or(false),
quantity: item.quantity,
category: item.category,
user_id: item.user_id,
product_upc: item.product_upc,
};
result.push(item);
}
}
result
}
fn transform_shared_users(users: Vec<PbEmailUserIdPair>) -> Vec<UserInfo> {
users
.into_iter()
.map(|user| UserInfo {
user_id: user.user_id.unwrap_or_default(),
email: user.email,
full_name: user.full_name,
})
.collect()
}
fn lists_from_response(response: PbShoppingListsResponse) -> Vec<List> {
let mut lists: Vec<List> = Vec::new();
for list in response.new_lists {
if let Some(name) = list.name {
let list = List {
id: list.identifier,
name,
items: transform_api_list_item(list.items),
shared_users: transform_shared_users(list.shared_users),
};
lists.push(list);
}
}
lists
}
#[cfg(test)]
mod tests {
use super::*;
use prost::Message;
#[test]
fn test_parse_list_with_shared_users() {
let snapshot_content =
include_str!("snapshots/webapp_captures__parse_list_with_shared_users.snap");
let response_hex = snapshot_content
.split("---")
.nth(2) .unwrap()
.trim();
let bytes = hex::decode(response_hex).unwrap();
let user_data = PbUserDataResponse::decode(bytes.as_ref()).unwrap();
let lists = lists_from_response(user_data.shopping_lists_response.unwrap());
assert!(!lists.is_empty(), "Should have at least one list");
let list_with_users = lists.iter().find(|l| !l.shared_users.is_empty());
assert!(
list_with_users.is_some(),
"Expected at least one list with shared users"
);
let list = list_with_users.unwrap();
assert!(
!list.shared_users.is_empty(),
"shared_users should not be empty"
);
let user = &list.shared_users[0];
assert!(!user.user_id.is_empty(), "user_id should be populated");
assert!(
user.email.is_some() || user.full_name.is_some(),
"Either email or full_name should be populated"
);
println!("✓ Found {} lists", lists.len());
println!(
"✓ List '{}' has {} shared users",
list.name,
list.shared_users.len()
);
for shared_user in &list.shared_users {
println!(" - user_id: {}", shared_user.user_id);
if let Some(email) = &shared_user.email {
println!(" email: {}", email);
}
if let Some(name) = &shared_user.full_name {
println!(" name: {}", name);
}
}
}
}