steam-user 0.1.0

Steam User web client for Rust - HTTP-based Steam Community interactions
Documentation
//! Detection of "container" inventory items (weapon cases, capsules,
//! sticker packs, gifts, etc.).

/// Market-hash names of well-known containers, used as a last-resort fallback
/// when an item's tags or `type` field don't carry the standard
/// `"Container"` / `"Base Grade Container"` markers.
///
/// In practice, items returned by Steam's inventory APIs always carry tag and
/// type metadata, so this list is rarely the deciding signal — but trade
/// history and partial trade-offer payloads occasionally strip those fields,
/// and the name fallback keeps detection robust in those cases.
///
/// Kept in sync with the case-image registry in
/// `steam-support-egui/src/ui/views/account_details/inventory.
/// rs::local_case_image`.
const KNOWN_CONTAINER_NAMES: &[&str] = &[
    // CS:GO / CS2 weapon cases (chronological)
    "CS:GO Weapon Case",
    "eSports 2013 Case",
    "Operation Bravo Case",
    "CS:GO Weapon Case 2",
    "eSports 2013 Winter Case",
    "Winter Offensive Weapon Case",
    "CS:GO Weapon Case 3",
    "Operation Phoenix Weapon Case",
    "Huntsman Weapon Case",
    "Operation Breakout Weapon Case",
    "eSports 2014 Summer Case",
    "Operation Vanguard Weapon Case",
    "Chroma Case",
    "Chroma 2 Case",
    "Falchion Case",
    "Shadow Case",
    "Revolver Case",
    "Operation Wildfire Case",
    "Chroma 3 Case",
    "Gamma Case",
    "Gamma 2 Case",
    "Glove Case",
    "Spectrum Case",
    "Operation Hydra Case",
    "Spectrum 2 Case",
    "Clutch Case",
    "CS20 Case",
    "Danger Zone Case",
    "Horizon Case",
    "Prisma Case",
    "Shattered Web Case",
    "Prisma 2 Case",
    "Fracture Case",
    "Operation Broken Fang Case",
    "Snakebite Case",
    "Operation Riptide Case",
    "Dreams & Nightmares Case",
    "Recoil Case",
    "Revolution Case",
    "Kilowatt Case",
    "Gallery Case",
    // Souvenir / sealed packages
    "Sealed Genesis Terminal",
    // Common sticker capsules
    "Sticker Capsule",
    "Sticker Capsule 2",
    "Community Sticker Capsule 1",
];

/// Returns `true` when an inventory item is a *container* — a weapon case,
/// capsule, sticker pack, gift, or other "openable" item.
///
/// Three signals are checked, in order of strength:
///
/// 1. **Item type label** — the Steam-rendered `type` string equals `"Base
///    Grade Container"`. This is the canonical type Steam attaches to standard
///    cases.
/// 2. **Tag metadata** — any tag has both `category == "Type"` and
///    `localized_tag_name == "Container"`. The strongest structural signal, but
///    uses the *localized* tag name, so callers must fetch inventories with
///    `l=english`.
/// 3. **Name fallback** — the item's `market_hash_name` (or `name`) appears in
///    a curated [`KNOWN_CONTAINER_NAMES`] list. Used when older or trimmed
///    payloads (e.g. trade-history HTML) drop tag/type metadata.
///
/// `tags` accepts any iterator yielding `(category, localized_tag_name)`
/// string pairs, so callers can adapt typed structs (`InventoryApiTag`,
/// `InventoryItemTag`, `InventoryHistoryTag`) and BSON tag documents alike
/// without copying.
pub fn is_inventory_container_item<'a, I>(item_type: &str, item_name: &str, tags: I) -> bool
where
    I: IntoIterator<Item = (&'a str, &'a str)>,
{
    if item_type == "Base Grade Container" {
        return true;
    }
    if tags.into_iter().any(|(category, localized_tag_name)| category == "Type" && localized_tag_name == "Container") {
        return true;
    }
    KNOWN_CONTAINER_NAMES.contains(&item_name)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn detects_via_item_type() {
        assert!(is_inventory_container_item("Base Grade Container", "Some Random Name", std::iter::empty::<(&str, &str)>()));
    }

    #[test]
    fn detects_via_tag() {
        assert!(is_inventory_container_item("", "Some Random Name", [("Type", "Container")]));
        assert!(!is_inventory_container_item("", "Some Random Name", [("Quality", "Container")]));
        assert!(!is_inventory_container_item("", "Some Random Name", [("Type", "Weapon")]));
    }

    /// Mirrors the canonical trade-history tag shape — exactly one tag with
    /// `category == "Type"` and `name == "Container"` must classify the item.
    #[test]
    fn detects_via_single_canonical_tag() {
        // Multiple tags, only one matches → still a container.
        let tags = [
            ("Rarity", "Mil-Spec"), // noise
            ("Quality", "Normal"),  // noise
            ("Type", "Container"),  // ← the canonical one
            ("Weapon", "AK-47"),    // noise
        ];
        assert!(is_inventory_container_item("", "Anything", tags));
    }

    #[test]
    fn detects_via_known_name() {
        assert!(is_inventory_container_item("", "Kilowatt Case", std::iter::empty::<(&str, &str)>()));
        assert!(!is_inventory_container_item("", "AK-47 | Redline", std::iter::empty::<(&str, &str)>()));
    }

    #[test]
    fn rejects_non_container() {
        assert!(!is_inventory_container_item("Rifle", "AK-47 | Redline", [("Type", "Weapon")]));
    }
}