use crate::protobuf::anylist::pb_operation_metadata::OperationClass;
use crate::protobuf::anylist::{
PbListFolderItem, PbListFolderOperation, PbListFolderOperationList, PbListOperation,
PbListOperationList, PbListSettings, PbListSettingsOperation, PbListSettingsOperationList,
PbOperationMetadata, PbShoppingList, PbStore, PbStoreFilter,
};
pub struct CreateListParams {
pub list_id: String,
pub operation_id: String,
pub user_id: String,
pub timestamp: f64,
pub name: String,
}
pub fn build_create_list_operation(params: CreateListParams) -> PbListOperationList {
let new_list = PbShoppingList {
identifier: params.list_id.clone(),
timestamp: Some(params.timestamp),
name: Some(params.name),
items: vec![],
creator: Some(params.user_id.clone()),
unusedattribute: vec![],
shared_users: vec![],
password: None,
notification_locations: vec![],
logical_clock_time: Some(1),
built_in_alexa_list_type: None,
allows_multiple_list_category_groups: Some(true),
list_item_sort_order: Some(0), new_list_item_position: Some(0), };
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("new-shopping-list".to_string()),
user_id: Some(params.user_id),
operation_class: Some(OperationClass::Undefined as i32),
}),
list_id: Some(params.list_id),
list: Some(new_list),
..Default::default()
};
PbListOperationList {
operations: vec![operation],
}
}
pub struct DeleteFolderItemsParams {
pub list_id: String,
pub list_data_id: String,
pub operation_id: String,
pub user_id: String,
}
pub fn build_delete_folder_items_operation(
params: DeleteFolderItemsParams,
) -> PbListFolderOperationList {
let folder_item = PbListFolderItem {
identifier: params.list_id,
item_type: Some(0),
};
let operation = PbListFolderOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("delete-folder-items".to_string()),
user_id: Some(params.user_id),
operation_class: Some(OperationClass::Undefined as i32),
}),
list_data_id: Some(params.list_data_id),
list_folder: None,
folder_items: vec![folder_item],
original_parent_folder_id: None,
updated_parent_folder_id: None,
};
PbListFolderOperationList {
operations: vec![operation],
}
}
pub struct RemoveListSettingsParams {
pub settings_id: String,
pub list_id: String,
pub operation_id: String,
pub user_id: String,
}
pub fn build_remove_list_settings_operation(
params: RemoveListSettingsParams,
) -> PbListSettingsOperationList {
let settings = PbListSettings {
identifier: params.settings_id,
user_id: Some(params.user_id.clone()),
list_id: Some(params.list_id),
timestamp: None,
should_hide_categories: None,
selected_category_ordering: None,
category_orderings: vec![],
generic_grocery_autocomplete_enabled: None,
list_item_sort_order: None,
category_grouping_id: None,
should_remember_item_categories: None,
favorites_autocomplete_enabled: None,
recent_items_autocomplete_enabled: None,
should_hide_completed_items: None,
list_color_type: None,
list_theme_id: None,
custom_theme: None,
badge_mode: None,
location_notifications_enabled: None,
store_filter_id: None,
should_hide_store_names: None,
should_hide_running_totals: None,
should_hide_prices: None,
left_running_total_type: None,
right_running_total_type: None,
linked_alexa_list_id: None,
list_category_group_id: None,
migration_list_category_group_id_for_new_list: None,
should_show_shared_list_category_order_hint_banner: None,
linked_google_assistant_list_id: None,
};
let operation = PbListSettingsOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("remove-list-settings".to_string()),
user_id: Some(params.user_id),
operation_class: Some(OperationClass::Undefined as i32),
}),
updated_settings: Some(settings),
};
PbListSettingsOperationList {
operations: vec![operation],
}
}
pub struct RenameListParams {
pub list_id: String,
pub operation_id: String,
pub user_id: String,
pub timestamp: f64,
pub old_name: String,
pub new_name: String,
}
pub fn build_rename_list_operation(params: RenameListParams) -> PbListOperationList {
let updated_list = PbShoppingList {
identifier: params.list_id.clone(),
timestamp: Some(params.timestamp),
name: Some(params.new_name),
items: vec![],
creator: Some(params.user_id.clone()),
unusedattribute: vec![],
shared_users: vec![],
password: None,
notification_locations: vec![],
logical_clock_time: None,
built_in_alexa_list_type: None,
allows_multiple_list_category_groups: Some(true),
list_item_sort_order: Some(0),
new_list_item_position: Some(0),
};
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("rename-list".to_string()),
user_id: Some(params.user_id),
operation_class: Some(OperationClass::Undefined as i32),
}),
list_id: Some(params.list_id),
original_value: Some(params.old_name),
list: Some(updated_list),
..Default::default()
};
PbListOperationList {
operations: vec![operation],
}
}
pub struct RemoveStoreFromItemsParams {
pub list_id: String,
pub store_id: String,
pub operation_id: String,
pub user_id: String,
}
pub fn build_remove_store_from_items_operation(
params: RemoveStoreFromItemsParams,
) -> PbListOperationList {
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("remove-store-id-from-all-items".to_string()),
user_id: Some(params.user_id),
operation_class: Some(OperationClass::Undefined as i32),
}),
list_id: Some(params.list_id),
updated_value: Some(params.store_id),
..Default::default()
};
PbListOperationList {
operations: vec![operation],
}
}
pub struct UpdateStoreFilterParams {
pub filter_id: String,
pub list_id: String,
pub filter_name: String,
pub store_ids: Vec<String>,
pub operation_id: String,
pub user_id: String,
}
pub fn build_update_store_filter_operation(params: UpdateStoreFilterParams) -> PbListOperationList {
let pb_filter = PbStoreFilter {
identifier: params.filter_id,
logical_timestamp: None,
list_id: Some(params.list_id.clone()),
name: Some(params.filter_name),
store_ids: params.store_ids,
includes_unassigned_items: None,
sort_index: None,
list_category_group_id: None,
shows_all_items: None,
};
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("update-store-filter".to_string()),
user_id: Some(params.user_id),
operation_class: Some(OperationClass::Undefined as i32),
}),
list_id: Some(params.list_id),
updated_store_filter: Some(pb_filter),
..Default::default()
};
PbListOperationList {
operations: vec![operation],
}
}
pub struct DeleteStoreFilterParams {
pub filter_id: String,
pub list_id: String,
pub filter_name: String,
pub operation_id: String,
pub user_id: String,
}
pub fn build_delete_store_filter_operation(params: DeleteStoreFilterParams) -> PbListOperationList {
let pb_filter = PbStoreFilter {
identifier: params.filter_id,
logical_timestamp: None,
list_id: Some(params.list_id.clone()),
name: Some(params.filter_name),
store_ids: vec![],
includes_unassigned_items: None,
sort_index: None,
list_category_group_id: None,
shows_all_items: None,
};
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("delete-store-filter".to_string()),
user_id: Some(params.user_id),
operation_class: Some(OperationClass::Undefined as i32),
}),
list_id: Some(params.list_id),
updated_store_filter: Some(pb_filter),
..Default::default()
};
PbListOperationList {
operations: vec![operation],
}
}
pub struct DeleteStoreParams {
pub store_id: String,
pub store_name: Option<String>,
pub list_id: String,
pub operation_id: String,
pub user_id: String,
}
pub fn build_delete_store_operation(params: DeleteStoreParams) -> PbListOperationList {
let pb_store = PbStore {
identifier: params.store_id,
name: params.store_name,
sort_index: None,
logical_timestamp: None,
list_id: Some(params.list_id.clone()),
};
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("delete-store".to_string()),
user_id: Some(params.user_id),
operation_class: Some(OperationClass::Undefined as i32),
}),
list_id: Some(params.list_id),
updated_store: Some(pb_store),
..Default::default()
};
PbListOperationList {
operations: vec![operation],
}
}
use crate::protobuf::anylist::{PbListItem, PbListItemCategoryAssignment};
pub struct AddItemParams {
pub item_id: String,
pub list_id: String,
pub operation_id: String,
pub user_id: String,
pub name: String,
pub category: Option<String>,
pub category_match_id: Option<String>,
pub category_assignment: Option<CategoryAssignment>,
}
pub struct CategoryAssignment {
pub identifier: String,
pub category_group_id: String,
pub category_id: String,
}
pub fn build_add_item_operation(params: AddItemParams) -> PbListOperationList {
let category_assignments = if let Some(assignment) = params.category_assignment {
vec![PbListItemCategoryAssignment {
identifier: Some(assignment.identifier),
category_group_id: Some(assignment.category_group_id),
category_id: Some(assignment.category_id),
}]
} else {
vec![]
};
let pb_item = PbListItem {
identifier: params.item_id.clone(),
server_mod_time: None,
list_id: Some(params.list_id.clone()),
name: Some(params.name),
quantity: None,
details: None,
checked: None,
recipe_id: None,
raw_ingredient: None,
price_matchup_tag: None,
price_id: None,
category: params.category,
user_id: Some(params.user_id.clone()),
category_match_id: params.category_match_id,
photo_ids: vec![],
event_id: None,
store_ids: vec![],
manual_sort_index: None,
prices: vec![],
category_assignments,
product_upc: None,
};
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("add-shopping-list-item".to_string()),
user_id: Some(params.user_id),
operation_class: None,
}),
list_id: Some(params.list_id),
list_item_id: Some(params.item_id),
list_item: Some(pb_item),
..Default::default()
};
PbListOperationList {
operations: vec![operation],
}
}
pub struct BulkRemoveItemsParams {
pub operation_id: String,
pub user_id: String,
pub list_id: String,
pub items: Vec<ItemToRemove>,
}
pub struct ItemToRemove {
pub item_id: String,
pub list_id: String,
pub name: String,
pub category: Option<String>,
pub user_id: String,
pub category_match_id: Option<String>,
pub category_assignment: Option<CategoryAssignment>,
}
pub fn build_bulk_remove_items_operation(params: BulkRemoveItemsParams) -> PbListOperationList {
let pb_items: Vec<PbListItem> = params
.items
.into_iter()
.map(|item| {
let category_assignments = if let Some(assignment) = item.category_assignment {
vec![PbListItemCategoryAssignment {
identifier: Some(assignment.identifier),
category_group_id: Some(assignment.category_group_id),
category_id: Some(assignment.category_id),
}]
} else {
vec![]
};
PbListItem {
identifier: item.item_id,
server_mod_time: None,
list_id: Some(item.list_id),
name: Some(item.name),
quantity: None,
details: None,
checked: None,
category: item.category,
user_id: Some(item.user_id),
recipe_id: None,
category_match_id: item.category_match_id,
raw_ingredient: None,
price_matchup_tag: None,
price_id: None,
photo_ids: vec![],
event_id: None,
store_ids: vec![],
manual_sort_index: None,
prices: vec![],
category_assignments,
product_upc: None,
}
})
.collect();
let pb_list = PbShoppingList {
identifier: params.list_id.clone(),
timestamp: None,
name: None,
items: pb_items,
creator: None,
unusedattribute: vec![],
shared_users: vec![],
password: None,
notification_locations: vec![],
logical_clock_time: None,
built_in_alexa_list_type: None,
allows_multiple_list_category_groups: None,
list_item_sort_order: None,
new_list_item_position: None,
};
let operation = PbListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("bulk-remove-list-items".to_string()),
user_id: Some(params.user_id),
operation_class: None,
}),
list_id: Some(params.list_id),
list: Some(pb_list),
..Default::default()
};
PbListOperationList {
operations: vec![operation],
}
}
use crate::protobuf::anylist::{PbStarterListOperation, PbStarterListOperationList};
pub struct AddFavouriteParams {
pub item_id: String,
pub list_id: String,
pub operation_id: String,
pub user_id: String,
pub name: String,
pub category: Option<String>,
}
pub fn build_add_favourite_operation(params: AddFavouriteParams) -> PbStarterListOperationList {
let pb_item = PbListItem {
identifier: params.item_id.clone(),
server_mod_time: None,
list_id: Some(params.list_id.clone()),
name: Some(params.name),
quantity: None,
details: None,
checked: None,
recipe_id: None,
raw_ingredient: None,
price_matchup_tag: None,
price_id: None,
category: params.category,
user_id: Some(params.user_id.clone()),
category_match_id: None,
photo_ids: vec![],
event_id: None,
store_ids: vec![],
manual_sort_index: None,
prices: vec![],
category_assignments: vec![],
product_upc: None,
};
let operation = PbStarterListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("add-starter-list-item".to_string()),
user_id: Some(params.user_id),
operation_class: None,
}),
list_id: Some(params.list_id),
list_item_id: Some(params.item_id),
list_item: Some(pb_item),
..Default::default()
};
PbStarterListOperationList {
operations: vec![operation],
}
}
pub struct RemoveFavouriteParams {
pub item_id: String,
pub list_id: String,
pub operation_id: String,
pub user_id: String,
}
pub fn build_remove_favourite_operation(params: RemoveFavouriteParams) -> PbStarterListOperationList {
let operation = PbStarterListOperation {
metadata: Some(PbOperationMetadata {
operation_id: Some(params.operation_id),
handler_id: Some("remove-starter-list-item".to_string()),
user_id: Some(params.user_id),
operation_class: None,
}),
list_id: Some(params.list_id),
list_item_id: Some(params.item_id),
..Default::default()
};
PbStarterListOperationList {
operations: vec![operation],
}
}
#[cfg(test)]
mod tests {
use super::*;
use prost::Message;
fn test_create_list_params() -> CreateListParams {
CreateListParams {
list_id: "test-list-abc123".to_string(),
operation_id: "test-op-xyz789".to_string(),
user_id: "test-user-456".to_string(),
timestamp: 1700000000.0,
name: "Groceries".to_string(),
}
}
#[test]
fn test_create_list_operation_snapshot() {
let params = test_create_list_params();
let operation_list = build_create_list_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_delete_folder_items_operation_snapshot() {
let params = DeleteFolderItemsParams {
list_id: "test-list-abc123".to_string(),
list_data_id: "test-list-data-id".to_string(),
operation_id: "test-op-delete-1".to_string(),
user_id: "test-user-456".to_string(),
};
let operation_list = build_delete_folder_items_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_remove_list_settings_operation_snapshot() {
let params = RemoveListSettingsParams {
settings_id: "test-settings-id".to_string(),
list_id: "test-list-abc123".to_string(),
operation_id: "test-op-remove-settings-1".to_string(),
user_id: "test-user-456".to_string(),
};
let operation_list = build_remove_list_settings_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_rename_list_operation_snapshot() {
let params = RenameListParams {
list_id: "test-list-abc123".to_string(),
operation_id: "test-op-rename-1".to_string(),
user_id: "test-user-456".to_string(),
timestamp: 1700000000.0,
old_name: "Old Name".to_string(),
new_name: "New Name".to_string(),
};
let operation_list = build_rename_list_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_remove_store_uses_updated_value_field() {
let params = RemoveStoreFromItemsParams {
list_id: "list-1".to_string(),
store_id: "store-abc".to_string(),
operation_id: "op-1".to_string(),
user_id: "user-1".to_string(),
};
let operation_list = build_remove_store_from_items_operation(params);
let op = &operation_list.operations[0];
assert_eq!(op.updated_value, Some("store-abc".to_string()));
assert_eq!(op.original_value, None);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_update_store_filter_operation_snapshot() {
let params = UpdateStoreFilterParams {
filter_id: "filter-123".to_string(),
list_id: "list-1".to_string(),
filter_name: "Weekend Stores".to_string(),
store_ids: vec!["store-a".to_string(), "store-b".to_string()],
operation_id: "op-update-filter-1".to_string(),
user_id: "user-1".to_string(),
};
let operation_list = build_update_store_filter_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_delete_store_filter_operation_snapshot() {
let params = DeleteStoreFilterParams {
filter_id: "filter-123".to_string(),
list_id: "list-1".to_string(),
filter_name: "Old Filter".to_string(),
operation_id: "op-delete-filter-1".to_string(),
user_id: "user-1".to_string(),
};
let operation_list = build_delete_store_filter_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_delete_store_uses_updated_store_field() {
let params = DeleteStoreParams {
store_id: "store-123".to_string(),
store_name: Some("Costco".to_string()),
list_id: "list-1".to_string(),
operation_id: "op-delete-store-1".to_string(),
user_id: "user-1".to_string(),
};
let operation_list = build_delete_store_operation(params);
let op = &operation_list.operations[0];
assert!(op.updated_store.is_some());
let store = op.updated_store.as_ref().unwrap();
assert_eq!(store.identifier, "store-123");
assert_eq!(store.name, Some("Costco".to_string()));
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_delete_store_without_name() {
let params = DeleteStoreParams {
store_id: "store-456".to_string(),
store_name: None,
list_id: "list-2".to_string(),
operation_id: "op-delete-store-2".to_string(),
user_id: "user-2".to_string(),
};
let operation_list = build_delete_store_operation(params);
let op = &operation_list.operations[0];
assert!(op.updated_store.is_some());
let store = op.updated_store.as_ref().unwrap();
assert_eq!(store.identifier, "store-456");
assert_eq!(store.name, None);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_webapp_add_shopping_list_item_2025_10_28() {
let params = AddItemParams {
item_id: "d83d31d5d48d4b7591b05c7efb725a46".to_string(),
list_id: "58ec2be417b247d7a9edc2d9d66889ab".to_string(),
operation_id: "0da34b3d00f54ce1bd6fd501ddf62f99".to_string(),
user_id: "cda21b0078644a01b640c84d3d74187e".to_string(),
name: "nice new things".to_string(),
category: Some("other".to_string()),
category_match_id: Some("other".to_string()),
category_assignment: Some(CategoryAssignment {
identifier: "47868d70669a5a078f8bc4e40dc07cab".to_string(),
category_group_id: "65564675a0de5a5fa6a69272df260fcc".to_string(),
category_id: "f962e619ab90479894c9dcc291a7f103".to_string(),
}),
};
let operation_list = build_add_item_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_webapp_bulk_remove_list_items_2025_10_28() {
let params = BulkRemoveItemsParams {
operation_id: "e26f61f101a94a81b395b4db8b870071".to_string(),
user_id: "cda21b0078644a01b640c84d3d74187e".to_string(),
list_id: "58ec2be417b247d7a9edc2d9d66889ab".to_string(),
items: vec![ItemToRemove {
item_id: "2eb63311646048568db569a50116cae8".to_string(),
list_id: "58ec2be417b247d7a9edc2d9d66889ab".to_string(),
name: "deleted item".to_string(),
category: Some("other".to_string()),
user_id: "cda21b0078644a01b640c84d3d74187e".to_string(),
category_match_id: Some("other".to_string()),
category_assignment: Some(CategoryAssignment {
identifier: "47868d70669a5a078f8bc4e40dc07cab".to_string(),
category_group_id: "65564675a0de5a5fa6a69272df260fcc".to_string(),
category_id: "f962e619ab90479894c9dcc291a7f103".to_string(),
}),
}],
};
let operation_list = build_bulk_remove_items_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_add_favourite_operation_snapshot() {
let params = AddFavouriteParams {
item_id: "fav-item-123".to_string(),
list_id: "fav-list-456".to_string(),
operation_id: "op-add-fav-1".to_string(),
user_id: "user-789".to_string(),
name: "Organic Milk".to_string(),
category: Some("Dairy".to_string()),
};
let operation_list = build_add_favourite_operation(params);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_add_favourite_operation_without_category() {
let params = AddFavouriteParams {
item_id: "fav-item-abc".to_string(),
list_id: "fav-list-def".to_string(),
operation_id: "op-add-fav-2".to_string(),
user_id: "user-xyz".to_string(),
name: "Bananas".to_string(),
category: None,
};
let operation_list = build_add_favourite_operation(params);
let op = &operation_list.operations[0];
assert_eq!(
op.metadata.as_ref().unwrap().handler_id,
Some("add-starter-list-item".to_string())
);
let item = op.list_item.as_ref().unwrap();
assert_eq!(item.category, None);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
#[test]
fn test_remove_favourite_operation_snapshot() {
let params = RemoveFavouriteParams {
item_id: "fav-item-to-remove".to_string(),
list_id: "fav-list-789".to_string(),
operation_id: "op-remove-fav-1".to_string(),
user_id: "user-abc".to_string(),
};
let operation_list = build_remove_favourite_operation(params);
let op = &operation_list.operations[0];
assert_eq!(
op.metadata.as_ref().unwrap().handler_id,
Some("remove-starter-list-item".to_string())
);
let mut buf = Vec::new();
operation_list.encode(&mut buf).unwrap();
insta::assert_snapshot!(hex::encode(&buf));
}
}