Skip to main content

steam_user/types/
ids.rs

1//! Type-safe newtypes for Steam economy identifiers.
2//!
3//! Each newtype wraps a primitive (`u32`/`u64`) to prevent argument-order
4//! mistakes at the call site. Functions like [`crate::SteamUserApi::sell_item`]
5//! accept five integer arguments — pre-newtype, every one of them was `u32` or
6//! `u64`, so a caller could swap `contextid` and `assetid` and the compiler
7//! would happily compile the bug.
8//!
9//! ## Serde behaviour
10//!
11//! Steam sometimes wire-encodes these as JSON numbers, sometimes as decimal
12//! strings (`"76561198..."`). The `Deserialize` impl accepts both. The
13//! `Serialize` impl always emits the numeric form — this is a one-way
14//! deserialization-bias choice; if a downstream system needs the string form
15//! it can use `to_string()`.
16
17use std::{fmt, str::FromStr};
18
19use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
20
21macro_rules! steam_id_newtype {
22    ($(#[$meta:meta])* $vis:vis $name:ident($inner:ty)) => {
23        $(#[$meta])*
24        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
25        $vis struct $name(pub $inner);
26
27        impl $name {
28            /// Wrap a raw integer in this newtype.
29            #[inline]
30            pub const fn new(inner: $inner) -> Self {
31                Self(inner)
32            }
33
34            /// Extract the raw integer.
35            #[inline]
36            pub const fn get(self) -> $inner {
37                self.0
38            }
39        }
40
41        impl From<$inner> for $name {
42            #[inline]
43            fn from(value: $inner) -> Self {
44                Self(value)
45            }
46        }
47
48        impl From<$name> for $inner {
49            #[inline]
50            fn from(value: $name) -> Self {
51                value.0
52            }
53        }
54
55        impl fmt::Display for $name {
56            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57                write!(f, "{}", self.0)
58            }
59        }
60
61        impl FromStr for $name {
62            type Err = std::num::ParseIntError;
63
64            fn from_str(s: &str) -> Result<Self, Self::Err> {
65                s.parse::<$inner>().map(Self)
66            }
67        }
68
69        impl Serialize for $name {
70            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
71                self.0.serialize(serializer)
72            }
73        }
74
75        impl<'de> Deserialize<'de> for $name {
76            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
77                // Accept either a JSON number or a decimal string. Steam mixes
78                // both representations across endpoints.
79                #[derive(Deserialize)]
80                #[serde(untagged)]
81                enum NumOrStr<T> {
82                    Num(T),
83                    Str(String),
84                }
85                let v = NumOrStr::<$inner>::deserialize(deserializer)?;
86                match v {
87                    NumOrStr::Num(n) => Ok(Self(n)),
88                    NumOrStr::Str(s) => s.parse::<$inner>().map(Self).map_err(de::Error::custom),
89                }
90            }
91        }
92    };
93}
94
95steam_id_newtype! {
96    /// Steam application ID (e.g. `730` for CS2, `570` for Dota 2).
97    pub AppId(u32)
98}
99
100steam_id_newtype! {
101    /// Inventory context ID inside a given app (e.g. `2` for CS2's "Backpack").
102    pub ContextId(u64)
103}
104
105steam_id_newtype! {
106    /// Unique asset ID for an inventory item instance.
107    pub AssetId(u64)
108}
109
110steam_id_newtype! {
111    /// Item class ID (groups items of the same template).
112    pub ClassId(u64)
113}
114
115steam_id_newtype! {
116    /// Item instance ID (variant within a class, e.g. wear/sticker config).
117    pub InstanceId(u64)
118}
119
120steam_id_newtype! {
121    /// Trade offer ID.
122    pub TradeOfferId(u64)
123}
124
125steam_id_newtype! {
126    /// Market item-orders histogram name ID.
127    pub ItemNameId(u64)
128}
129
130steam_id_newtype! {
131    /// Quantity of an item involved in a market sell / trade action.
132    pub Amount(u32)
133}
134
135steam_id_newtype! {
136    /// Price expressed in the smallest currency unit Steam uses (e.g. cents).
137    pub PriceCents(u32)
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn parses_number_or_string() {
146        let n: AssetId = serde_json::from_str("42").unwrap();
147        let s: AssetId = serde_json::from_str("\"42\"").unwrap();
148        assert_eq!(n, AssetId(42));
149        assert_eq!(s, AssetId(42));
150    }
151
152    #[test]
153    fn serializes_as_number() {
154        let id = AssetId(42);
155        assert_eq!(serde_json::to_string(&id).unwrap(), "42");
156    }
157
158    #[test]
159    fn ergonomic_conversions() {
160        let id: AppId = 730u32.into();
161        assert_eq!(u32::from(id), 730);
162        assert_eq!(id.to_string(), "730");
163        assert_eq!("730".parse::<AppId>().unwrap(), AppId(730));
164    }
165}