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 }