1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
use core::fmt;
use chrono::{DateTime, Duration, Utc};
use serde::Deserialize;
use super::{CollectionItemType, GameVersion, ItemFamilyRank, XmlVersions};
use crate::deserialize::{
deserialize_1_0_bool, deserialize_date_time, deserialize_date_time_with_zone,
deserialize_minutes, xml_ranks_to_ranks, XmlFloatValue, XmlIntValue, XmlRanks,
};
/// A user's collection on boardgamegeek.
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct Collection<T> {
/// List of games, expansions, and accessories in the user's collection. Each game
/// is not necessarily owned but can be preowned, on the user's wishlist etc.
///
/// Note that accessories and games can never be returned together in one collection,
/// but games and game expansions can.
#[serde(default = "Vec::new", rename = "item")]
pub items: Vec<T>,
/// Date and time at which this collection was published.
///
/// When a user's collection is requested, if the data is not ready the request will be queued
/// and a 202 accepted status will be returned, with a message to
#[serde(
rename = "pubdate",
deserialize_with = "deserialize_date_time_with_zone"
)]
pub published_date: DateTime<Utc>,
}
/// An item in a collection, in brief form. With the name, status, type,
/// and IDs, also a brief version of the game stats.
///
/// If requested and applicable, version information is also included,
/// this will be the same information as is included in the full version.
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct CollectionItemBrief {
/// The ID of the item.
#[serde(rename = "objectid")]
pub id: u64,
/// The collection ID of the object.
#[serde(rename = "collid")]
pub collection_id: u64,
/// The type of collection item, which will either be boardgame, expansion, or accessory.
#[serde(rename = "subtype")]
pub item_type: CollectionItemType,
/// The name of the item.
pub name: String,
/// Status of the item in this collection, such as own, preowned, wishlist.
pub status: CollectionItemStatus,
/// Game stats such as number of players.
pub stats: CollectionItemStatsBrief,
/// Information about this version of the game. Only included if version
/// information is requested and also if the game is an alternate
/// version of another game.
#[serde(default, deserialize_with = "deserialize_version_list")]
pub version: Option<GameVersion>,
}
/// A game, game expansion, or game accessory in a collection.
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct CollectionItem {
/// The ID of the item.
#[serde(rename = "objectid")]
pub id: u64,
/// The collection ID of the object.
#[serde(rename = "collid")]
pub collection_id: u64,
/// The type of collection item, which will either be boardgame, expansion, or accessory.
#[serde(rename = "subtype")]
pub item_type: CollectionItemType,
/// The name of the item.
pub name: String,
/// The year the item was first published. Can be empty.
#[serde(rename = "yearpublished")]
pub year_published: Option<i64>,
/// A link to a jpg image for the item. Can be empty.
pub image: Option<String>,
/// A link to a jpg thumbnail image for the item. Can be empty.
pub thumbnail: Option<String>,
/// Status of the item in this collection, such as own, preowned, wishlist.
pub status: CollectionItemStatus,
/// The number of times the user has played the game.
#[serde(rename = "numplays")]
pub number_of_plays: u64,
/// Game stats such as number of players.
pub stats: CollectionItemStats,
/// Information about this version of the game. Only included if version
/// information is requested and also if the game is an alternate
/// version of another game.
#[serde(default, deserialize_with = "deserialize_version_list")]
pub version: Option<GameVersion>,
}
/// The status of the item in the user's collection, such as preowned or
/// wishlist. Can be any or none of them.
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct CollectionItemStatus {
/// User owns the item.
#[serde(deserialize_with = "deserialize_1_0_bool")]
pub own: bool,
/// User has previously owned the item.
#[serde(rename = "prevowned", deserialize_with = "deserialize_1_0_bool")]
pub previously_owned: bool,
/// User wants to trade away the item.
#[serde(rename = "fortrade", deserialize_with = "deserialize_1_0_bool")]
pub for_trade: bool,
/// User wants to receive the item in a trade.
#[serde(rename = "want", deserialize_with = "deserialize_1_0_bool")]
pub want_in_trade: bool,
/// User wants to play the item.
#[serde(rename = "wanttoplay", deserialize_with = "deserialize_1_0_bool")]
pub want_to_play: bool,
/// User wants to buy the item.
#[serde(rename = "wanttobuy", deserialize_with = "deserialize_1_0_bool")]
pub want_to_buy: bool,
/// User pre-ordered the item.
#[serde(rename = "preordered", deserialize_with = "deserialize_1_0_bool")]
pub pre_ordered: bool,
/// User has the item on their wishlist.
#[serde(deserialize_with = "deserialize_1_0_bool")]
pub wishlist: bool,
/// The priority of the wishlist.
#[serde(default, rename = "wishlistpriority")]
pub wishlist_priority: Option<WishlistPriority>,
/// When the collection status was last modified.
#[serde(rename = "lastmodified", deserialize_with = "deserialize_date_time")]
pub last_modified: DateTime<Utc>,
}
/// The status of the item in the user's collection, such as preowned or
/// wishlist. Can be any or none of them.
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub enum WishlistPriority {
/// Lowest priority.
DontBuyThis,
/// Thinking about buying it.
ThinkingAboutIt,
/// The default value, would like to have it.
LikeToHave,
/// Would love to have it.
LoveToHave,
/// Highest wishlist priority, a must have.
MustHave,
}
impl<'de> Deserialize<'de> for WishlistPriority {
fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s: String = serde::de::Deserialize::deserialize(deserializer)?;
match s.as_str() {
"5" => Ok(WishlistPriority::DontBuyThis),
"4" => Ok(WishlistPriority::ThinkingAboutIt),
"3" => Ok(WishlistPriority::LikeToHave),
"2" => Ok(WishlistPriority::LoveToHave),
"1" => Ok(WishlistPriority::MustHave),
s => Err(serde::de::Error::custom(format!(
"invalid value for wishlist priority, expected \"1\" - \"5\" but got {s}"
))),
}
}
}
pub(crate) fn deserialize_version_list<'de, D>(
deserializer: D,
) -> Result<Option<GameVersion>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
// If the tag is missing the None case is already handled by the `#serde[(default)]` on
// the type. If this deserialize returns an error it should be propagated.
let mut versions: XmlVersions = serde::de::Deserialize::deserialize(deserializer)?;
match versions.versions.len() {
0 => Err(serde::de::Error::custom(format!(
"empty version list found for game ID {}, expected \"1\"",
versions.versions[0].id,
))),
1 => Ok(Some(versions.versions.remove(0))),
len => Err(serde::de::Error::custom(format!(
"invalid number of versions found for game ID {}, expected \"1\", but got {}",
versions.versions[0].id, len,
))),
}
}
/// Stats of the game such as player count and duration. Can be omitted from the
/// response. More stats can be found from the specific game endpoint.
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct CollectionItemStatsBrief {
/// Minimum players the game supports.
#[serde(default, rename = "minplayers")]
pub min_players: u32,
/// Maximum players the game supports.
#[serde(default, rename = "maxplayers")]
pub max_players: u32,
/// Minimum amount of time the game is suggested to take to play.
#[serde(
default,
rename = "minplaytime",
deserialize_with = "deserialize_minutes"
)]
pub min_playtime: Duration,
/// Maximum amount of time the game is suggested to take to play.
#[serde(
default,
rename = "maxplaytime",
deserialize_with = "deserialize_minutes"
)]
pub max_playtime: Duration,
/// The amount of time the game is suggested to take to play.
#[serde(
default,
rename = "playingtime",
deserialize_with = "deserialize_minutes"
)]
pub playing_time: Duration,
/// The number of people that own this game.
#[serde(rename = "numowned")]
pub owned_by: u64,
/// Information about the rating that this user, as well as all users, have
/// given this game.
pub rating: CollectionItemRatingBrief,
}
/// Stats of the game such as the player count and duration. Can be omitted from the
/// response. More stats can be found from the specific game endpoint.
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct CollectionItemStats {
/// Minimum players the game supports.
#[serde(default, rename = "minplayers")]
pub min_players: u32,
/// Maximum players the game supports.
#[serde(default, rename = "maxplayers")]
pub max_players: u32,
/// Minimum amount of time the game is suggested to take to play.
#[serde(
default,
rename = "minplaytime",
deserialize_with = "deserialize_minutes"
)]
pub min_playtime: Duration,
/// Maximum amount of time the game is suggested to take to play.
#[serde(
default,
rename = "maxplaytime",
deserialize_with = "deserialize_minutes"
)]
pub max_playtime: Duration,
/// The amount of time the game is suggested to take to play.
#[serde(
default,
rename = "playingtime",
deserialize_with = "deserialize_minutes"
)]
pub playing_time: Duration,
/// The number of people that own this game.
#[serde(rename = "numowned")]
pub owned_by: u64,
/// Information about the rating that this user, as well as all users, have
/// given this game.
#[serde(rename = "rating")]
pub rating: CollectionItemRating,
}
/// The 0-10 rating that the user gave to this item. Also includes the total
/// number of users that have rated it, as well as the averages.
#[derive(Clone, Debug, PartialEq)]
pub struct CollectionItemRatingBrief {
/// The 0-10 rating that the user gave this item.
pub user_rating: Option<f64>,
/// The mean average rating for this item.
pub average: f64,
/// The bayesian average rating for this item. Will be set to 0 if the item does not
/// yet have a bayesian rating.
pub bayesian_average: f64,
}
impl<'de> Deserialize<'de> for CollectionItemRatingBrief {
fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Value,
Average,
BayesAverage,
}
struct CollectionItemRatingBriefVisitor;
impl<'de> serde::de::Visitor<'de> for CollectionItemRatingBriefVisitor {
type Value = CollectionItemRatingBrief;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string containing the XML for a user's rating of a board game, which includes the average rating on the site and the number of ratings.")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut user_rating = None;
let mut average = None;
let mut bayesian_average = None;
while let Some(key) = map.next_key()? {
match key {
Field::Value => {
if user_rating.is_some() {
return Err(serde::de::Error::duplicate_field("value"));
}
let user_rating_str: String = map.next_value()?;
user_rating = match user_rating_str.as_str() {
"N/A" => Some(None),
other => Some(Some(other.parse::<f64>().map_err(|e| {
serde::de::Error::custom(format!(
"failed to parse value as N/A or float: {e}"
))
})?)),
}
},
Field::Average => {
if average.is_some() {
return Err(serde::de::Error::duplicate_field("average"));
}
let average_xml_tag: XmlFloatValue = map.next_value()?;
average = Some(average_xml_tag.value);
},
Field::BayesAverage => {
if bayesian_average.is_some() {
return Err(serde::de::Error::duplicate_field("bayesaverage"));
}
let bayesian_average_xml_tag: XmlFloatValue = map.next_value()?;
bayesian_average = Some(bayesian_average_xml_tag.value);
},
}
}
let user_rating =
user_rating.ok_or_else(|| serde::de::Error::missing_field("value"))?;
let average = average.ok_or_else(|| serde::de::Error::missing_field("average"))?;
let bayesian_average = bayesian_average
.ok_or_else(|| serde::de::Error::missing_field("bayesaverage"))?;
Ok(Self::Value {
user_rating,
average,
bayesian_average,
})
}
}
deserializer.deserialize_any(CollectionItemRatingBriefVisitor)
}
}
/// The 0-10 rating that the user gave to this item. Also includes the total
/// number of users that have rated it, as well as the averages, and standard
/// deviation.
#[derive(Clone, Debug, PartialEq)]
pub struct CollectionItemRating {
/// The 0-10 rating that the user gave this item.
pub user_rating: Option<f64>,
/// The total number of users who have given this item a rating.
pub users_rated: u64,
/// The mean average rating for this item.
pub average: f64,
/// The bayesian average rating for this item. Will be set to 0 if the item does not
/// yet have a bayesian rating.
pub bayesian_average: f64,
/// The standard deviation of the average rating.
pub standard_deviation: f64,
// Kept private for now since the API always returns 0 for this seemingly.
pub(crate) median: f64,
/// The rank of this item amongst everything of that item type.
pub rank: ItemFamilyRank,
/// The list of ranks the item is on the site within various game families, such as family
/// games.
pub sub_family_ranks: Vec<ItemFamilyRank>,
}
impl<'de> Deserialize<'de> for CollectionItemRating {
fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Value,
UsersRated,
Average,
Bayesaverage,
StdDev,
Median,
Ranks,
}
struct CollectionItemRatingVisitor;
impl<'de> serde::de::Visitor<'de> for CollectionItemRatingVisitor {
type Value = CollectionItemRating;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string containing the XML for a user's rating of a board game, which includes the average rating on the site and the number of ratings.")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut user_rating = None;
let mut users_rated = None;
let mut average = None;
let mut bayesian_average = None;
let mut standard_deviation = None;
let mut median = None;
let mut rank = None;
let mut sub_family_ranks = vec![];
while let Some(key) = map.next_key()? {
match key {
Field::Value => {
if user_rating.is_some() {
return Err(serde::de::Error::duplicate_field("value"));
}
let user_rating_str: String = map.next_value()?;
user_rating = match user_rating_str.as_str() {
"N/A" => Some(None),
other => Some(Some(other.parse::<f64>().map_err(|e| {
serde::de::Error::custom(format!(
"failed to parse value as N/A or float: {e}"
))
})?)),
}
},
Field::UsersRated => {
if users_rated.is_some() {
return Err(serde::de::Error::duplicate_field("usersrated"));
}
let users_rated_xml_tag: XmlIntValue = map.next_value()?;
users_rated = Some(users_rated_xml_tag.value);
},
Field::Average => {
if average.is_some() {
return Err(serde::de::Error::duplicate_field("average"));
}
let average_xml_tag: XmlFloatValue = map.next_value()?;
average = Some(average_xml_tag.value);
},
Field::Bayesaverage => {
if bayesian_average.is_some() {
return Err(serde::de::Error::duplicate_field("bayesaverage"));
}
let bayesian_average_xml_tag: XmlFloatValue = map.next_value()?;
bayesian_average = Some(bayesian_average_xml_tag.value);
},
Field::StdDev => {
if standard_deviation.is_some() {
return Err(serde::de::Error::duplicate_field("stddev"));
}
let standard_deviation_xml_tag: XmlFloatValue = map.next_value()?;
standard_deviation = Some(standard_deviation_xml_tag.value);
},
Field::Median => {
if median.is_some() {
return Err(serde::de::Error::duplicate_field("median"));
}
let median_xml_tag: XmlFloatValue = map.next_value()?;
median = Some(median_xml_tag.value);
},
Field::Ranks => {
let ranks_xml: XmlRanks = map.next_value()?;
let (overall_rank, other_ranks) =
xml_ranks_to_ranks::<'de, A>(ranks_xml)?;
rank = Some(overall_rank);
sub_family_ranks = other_ranks;
},
}
}
let user_rating =
user_rating.ok_or_else(|| serde::de::Error::missing_field("value"))?;
let users_rated =
users_rated.ok_or_else(|| serde::de::Error::missing_field("usersrated"))?;
let average = average.ok_or_else(|| serde::de::Error::missing_field("average"))?;
let bayesian_average = bayesian_average
.ok_or_else(|| serde::de::Error::missing_field("bayesaverage"))?;
let standard_deviation =
standard_deviation.ok_or_else(|| serde::de::Error::missing_field("stddev"))?;
let median = median.ok_or_else(|| serde::de::Error::missing_field("median"))?;
let rank =
rank.ok_or_else(|| serde::de::Error::missing_field("rank type=\"subtype\""))?;
Ok(Self::Value {
user_rating,
users_rated,
average,
bayesian_average,
standard_deviation,
median,
rank,
sub_family_ranks,
})
}
}
deserializer.deserialize_any(CollectionItemRatingVisitor)
}
}
/// A rank a particular board game has on the site, within a subtype. Can be
/// either Ranked with a u64 for the rank, Or `NotRanked`.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum RankValue {
/// The rank of a game within a particular family of games, or all games. Where
/// 1 means that it has the highest overall rank of every game in that category.
Ranked(u64),
/// The game does not have a rank in a given category, possibly due to not having
/// enough ratings.
NotRanked,
}
impl<'de> Deserialize<'de> for RankValue {
fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s: String = serde::de::Deserialize::deserialize(deserializer)?;
if s == "Not Ranked" {
return Ok(RankValue::NotRanked);
}
let rank: Result<u64, _> = s.parse();
match rank {
Ok(value) => Ok(RankValue::Ranked(value)),
_ => Err(serde::de::Error::unknown_variant(
&s,
&["u64", "Not Ranked"],
)),
}
}
}
/// A Bayesian average rating of a boardgame in its family.
/// Either valued as a f64, or `NotRanked`.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum RatingValue {
/// The Bayesian average rating of a game within a specific family or category.
///
/// The `f64` value represents the calculated Bayesian average. A higher value generally
/// indicates a better-rated game.
Rated(f64),
/// Indicates that the game does not have a Bayesian average rating in the given category.
///
/// This may occur if the game has insufficient ratings to calculate a reliable average or
/// if it is excluded from the ranking system for other reasons.
Unrated,
}
impl<'de> Deserialize<'de> for RatingValue {
fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s: String = serde::de::Deserialize::deserialize(deserializer)?;
if s == "Not Ranked" {
return Ok(RatingValue::Unrated);
}
let rank: Result<f64, _> = s.parse();
match rank {
Ok(value) => Ok(RatingValue::Rated(value)),
_ => Err(serde::de::Error::unknown_variant(
&s,
&["f64", "Not Ranked"],
)),
}
}
}