/ src / steam / data / user.rs
user.rs
  1  use anyhow::Result;
  2  use serde::{Deserialize, Serialize};
  3  use serde_json::Value;
  4  use crate::steam::{SteamAchievement, platform::Payload_GetOwnedGames};
  5  use super::game::Game;
  6  
  7  /**
  8  Profile information for a Steam user.
  9  */
 10  #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
 11  pub struct SteamUser
 12  {
 13  	/**
 14  	The list of games associated with this user which also have achievements
 15  	defined, across all platforms.
 16  	*/
 17  	#[serde(default)]
 18  	pub games: Vec<Game>,
 19  	
 20  	/// The path to the user's avatar
 21  	#[serde(default)]
 22  	pub avatar: Option<String>,
 23  	
 24  	/// The user's 64-bit Steam ID
 25  	pub id: String,
 26  	
 27  	/// The user's current publicly visible display name.
 28  	#[serde(default)]
 29  	pub name: String,
 30  }
 31  
 32  impl SteamUser
 33  {
 34  	pub const FileName: &str = "steam.json";
 35  	
 36  	/**
 37  	Parse a JSON string which does not strictly conform to the expected `User`
 38  	data structure.
 39  	
 40  	This function will retain as much data as possible but will omit objects if
 41  	they are missing required properties.
 42  	
 43  	Any missing properties which are not strictly required will instead be
 44  	filled with their default values.
 45  	
 46  	Only returns `Err` if `json` is not valid JSON.
 47  	*/
 48  	pub fn parseJsonLossy(json: String) -> Result<Self>
 49  	{
 50  		let root = serde_json::from_str::<Value>(json.as_str())?;
 51  		
 52  		let mut user = Self::default();
 53  		
 54  		match root
 55  		{
 56  			Value::Object(map) => {
 57  				
 58  				if let Some((_, value)) = map.iter()
 59  					.find(|(k, _)| k.as_str() == "games")
 60  				{
 61  					if let Value::Array(inner) = value
 62  					{
 63  						let mut parsedGames = vec![];
 64  						for gameValue in inner
 65  						{
 66  							if let Value::Object(gameMap) = gameValue
 67  							{
 68  								if let Some(game) = Game::parseJsonMap(gameMap)
 69  								{
 70  									parsedGames.push(game);
 71  								}
 72  							}
 73  						}
 74  						
 75  						user.games = parsedGames;
 76  					}
 77  				}
 78  				
 79  				if let Some((_, value)) = map.iter()
 80  					.find(|(k, _)| k.as_str() == "avatar")
 81  				{
 82  					if let Value::String(inner) = value
 83  					{
 84  						if !inner.is_empty()
 85  						{
 86  							user.avatar = Some(inner.clone());
 87  						}
 88  					}
 89  				}
 90  				
 91  				if let Some((_, value)) = map.iter()
 92  					.find(|(k, _)| k.as_str() == "id")
 93  				{
 94  					if let Value::String(inner) = value
 95  					{
 96  						user.id = inner.clone();
 97  					}
 98  				}
 99  				
100  				if let Some((_, value)) = map.iter()
101  					.find(|(k, _)| k.as_str() == "name")
102  				{
103  					if let Value::String(value) = value
104  					{
105  						user.name = value.clone();
106  					}
107  				}
108  			},
109  			
110  			_ => {},
111  		}
112  		
113  		return Ok(user);
114  	}
115  	
116  	pub fn filterGames(&self, search: impl Into<String>) -> Vec<Game>
117  	{
118  		let text = search.into().to_lowercase();
119  		let mut games = self.games.iter()
120  			.filter(|g| g.name.to_lowercase().contains(&text))
121  			.cloned()
122  			.collect::<Vec<_>>();
123  		games.sort();
124  		
125  		return games;
126  	}
127  	
128  	pub fn getGame(&self, appId: u64) -> Option<Game>
129  	{
130  		return self.games.iter()
131  			.find(|g| g.id == appId)
132  			.cloned();
133  	}
134  	
135  	pub fn getAchievement(&self, appId: impl Into<u64>, id: impl Into<String>) -> Option<SteamAchievement>
136  	{
137  		let achievementId = id.into();
138  		return match self.getGame(appId.into())
139  		{
140  			None => None,
141  			Some(g) => g.achievements.iter()
142  				.find(|a| a.id == achievementId)
143  				.cloned(),
144  		};
145  	}
146  	
147  	pub fn processOwnedGames(&mut self, payload: Payload_GetOwnedGames)
148  	{
149  		for game in payload.response.games
150  		{
151  			match self.games.iter_mut()
152  				.find(|g| g.id == game.appid)
153  			{
154  				None => self.games.push(game.clone().into()),
155  				Some(g) => g.update(&game),
156  			}
157  		}
158  	}
159  	
160  	pub fn update(&mut self, id: &String, name: &String, avatar: Option<&String>)
161  	{
162  		self.id = id.clone();
163  		self.name = name.clone();
164  		self.avatar = avatar.cloned();
165  	}
166  }
167  
168  #[cfg(test)]
169  mod tests
170  {
171  	use super::*;
172  	
173  	const PartialJson: &str = r#"{
174  	"games": [
175  		{
176  			"id": 73,
177  			"name": "First game",
178  			"achievements": [
179  				{ "id": "chievo1", "name": "Successful parse!" },
180  				{ "name": "This one should fail to parse" }
181  			]
182  		},
183  		
184  		{
185  			"name": "Test game that shouldn't parse",
186  			"achievements": [
187  				{ "id": 4, "name": "Successful parse!" }
188  			]
189  		}
190  	],
191  	"avatar": "The avatar",
192  	"id": "The id",
193  	"name": "The name"
194  }"#;
195  	
196  	#[test]
197  	fn parseJsonLossy()
198  	{
199  		let result = SteamUser::parseJsonLossy(PartialJson.into());
200  		assert!(result.is_ok());
201  		
202  		let user = result.unwrap();
203  		assert_eq!(user.avatar, Some("The avatar".to_string()));
204  		assert_eq!(user.id, "The id".to_string());
205  		assert_eq!(user.name, "The name".to_string());
206  		assert_eq!(user.games.len(), 1);
207  		
208  		let game = user.games.first().unwrap();
209  		assert_eq!(game.id, 73);
210  		assert_eq!(game.name, "First game".to_string());
211  		assert_eq!(game.achievements.len(), 1);
212  		
213  		let trophy = game.achievements.first().unwrap();
214  		assert_eq!(trophy.id, "chievo1".to_string());
215  		assert_eq!(trophy.name, "Successful parse!".to_string());
216  	}
217  }