steam-user 0.1.0

Steam User web client for Rust - HTTP-based Steam Community interactions
Documentation
//! Type-safe newtypes for Steam economy identifiers.
//!
//! Each newtype wraps a primitive (`u32`/`u64`) to prevent argument-order
//! mistakes at the call site. Functions like [`crate::SteamUserApi::sell_item`]
//! accept five integer arguments — pre-newtype, every one of them was `u32` or
//! `u64`, so a caller could swap `contextid` and `assetid` and the compiler
//! would happily compile the bug.
//!
//! ## Serde behaviour
//!
//! Steam sometimes wire-encodes these as JSON numbers, sometimes as decimal
//! strings (`"76561198..."`). The `Deserialize` impl accepts both. The
//! `Serialize` impl always emits the numeric form — this is a one-way
//! deserialization-bias choice; if a downstream system needs the string form
//! it can use `to_string()`.

use std::{fmt, str::FromStr};

use serde::{de, Deserialize, Deserializer, Serialize, Serializer};

macro_rules! steam_id_newtype {
    ($(#[$meta:meta])* $vis:vis $name:ident($inner:ty)) => {
        $(#[$meta])*
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
        $vis struct $name(pub $inner);

        impl $name {
            /// Wrap a raw integer in this newtype.
            #[inline]
            pub const fn new(inner: $inner) -> Self {
                Self(inner)
            }

            /// Extract the raw integer.
            #[inline]
            pub const fn get(self) -> $inner {
                self.0
            }
        }

        impl From<$inner> for $name {
            #[inline]
            fn from(value: $inner) -> Self {
                Self(value)
            }
        }

        impl From<$name> for $inner {
            #[inline]
            fn from(value: $name) -> Self {
                value.0
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "{}", self.0)
            }
        }

        impl FromStr for $name {
            type Err = std::num::ParseIntError;

            fn from_str(s: &str) -> Result<Self, Self::Err> {
                s.parse::<$inner>().map(Self)
            }
        }

        impl Serialize for $name {
            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
                self.0.serialize(serializer)
            }
        }

        impl<'de> Deserialize<'de> for $name {
            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
                // Accept either a JSON number or a decimal string. Steam mixes
                // both representations across endpoints.
                #[derive(Deserialize)]
                #[serde(untagged)]
                enum NumOrStr<T> {
                    Num(T),
                    Str(String),
                }
                let v = NumOrStr::<$inner>::deserialize(deserializer)?;
                match v {
                    NumOrStr::Num(n) => Ok(Self(n)),
                    NumOrStr::Str(s) => s.parse::<$inner>().map(Self).map_err(de::Error::custom),
                }
            }
        }
    };
}

steam_id_newtype! {
    /// Steam application ID (e.g. `730` for CS2, `570` for Dota 2).
    pub AppId(u32)
}

steam_id_newtype! {
    /// Inventory context ID inside a given app (e.g. `2` for CS2's "Backpack").
    pub ContextId(u64)
}

steam_id_newtype! {
    /// Unique asset ID for an inventory item instance.
    pub AssetId(u64)
}

steam_id_newtype! {
    /// Item class ID (groups items of the same template).
    pub ClassId(u64)
}

steam_id_newtype! {
    /// Item instance ID (variant within a class, e.g. wear/sticker config).
    pub InstanceId(u64)
}

steam_id_newtype! {
    /// Trade offer ID.
    pub TradeOfferId(u64)
}

steam_id_newtype! {
    /// Market item-orders histogram name ID.
    pub ItemNameId(u64)
}

steam_id_newtype! {
    /// Quantity of an item involved in a market sell / trade action.
    pub Amount(u32)
}

steam_id_newtype! {
    /// Price expressed in the smallest currency unit Steam uses (e.g. cents).
    pub PriceCents(u32)
}

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

    #[test]
    fn parses_number_or_string() {
        let n: AssetId = serde_json::from_str("42").unwrap();
        let s: AssetId = serde_json::from_str("\"42\"").unwrap();
        assert_eq!(n, AssetId(42));
        assert_eq!(s, AssetId(42));
    }

    #[test]
    fn serializes_as_number() {
        let id = AssetId(42);
        assert_eq!(serde_json::to_string(&id).unwrap(), "42");
    }

    #[test]
    fn ergonomic_conversions() {
        let id: AppId = 730u32.into();
        assert_eq!(u32::from(id), 730);
        assert_eq!(id.to_string(), "730");
        assert_eq!("730".parse::<AppId>().unwrap(), AppId(730));
    }
}