mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-05-24 09:10:49 +00:00
Move to std::time::Duration
... to allow serde on all youmubot-osu structs
This commit is contained in:
parent
3af0b56755
commit
c7da9526e6
8 changed files with 202 additions and 88 deletions
|
@ -10,12 +10,23 @@ use request::builders::*;
|
|||
use request::*;
|
||||
use reqwest::Client as HTTPClient;
|
||||
use serenity::framework::standard::CommandError as Error;
|
||||
use std::convert::TryInto;
|
||||
|
||||
/// Client is the client that will perform calls to the osu! api server.
|
||||
pub struct Client {
|
||||
key: String,
|
||||
}
|
||||
|
||||
fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>, T::Error> {
|
||||
let mut res = Vec::with_capacity(v.len());
|
||||
|
||||
for u in v.into_iter() {
|
||||
res.push(u.try_into()?);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client from the given API key.
|
||||
pub fn new(key: impl AsRef<str>) -> Client {
|
||||
|
@ -42,8 +53,8 @@ impl Client {
|
|||
) -> Result<Vec<Beatmap>, Error> {
|
||||
let mut r = BeatmapRequestBuilder::new(kind);
|
||||
f(&mut r);
|
||||
let res = self.build_request(client, r.build(client))?.json()?;
|
||||
Ok(res)
|
||||
let res: Vec<raw::Beatmap> = self.build_request(client, r.build(client))?.json()?;
|
||||
Ok(vec_try_into(res)?)
|
||||
}
|
||||
|
||||
pub fn user(
|
||||
|
@ -54,7 +65,8 @@ impl Client {
|
|||
) -> Result<Option<User>, Error> {
|
||||
let mut r = UserRequestBuilder::new(user);
|
||||
f(&mut r);
|
||||
let res: Vec<_> = self.build_request(client, r.build(client))?.json()?;
|
||||
let res: Vec<raw::User> = self.build_request(client, r.build(client))?.json()?;
|
||||
let res = vec_try_into(res)?;
|
||||
Ok(res.into_iter().next())
|
||||
}
|
||||
|
||||
|
@ -66,7 +78,8 @@ impl Client {
|
|||
) -> Result<Vec<Score>, Error> {
|
||||
let mut r = ScoreRequestBuilder::new(beatmap_id);
|
||||
f(&mut r);
|
||||
let mut res: Vec<Score> = self.build_request(client, r.build(client))?.json()?;
|
||||
let res: Vec<raw::Score> = self.build_request(client, r.build(client))?.json()?;
|
||||
let mut res: Vec<Score> = vec_try_into(res)?;
|
||||
|
||||
// with a scores request you need to fill the beatmap ids yourself
|
||||
res.iter_mut().for_each(|v| {
|
||||
|
@ -102,7 +115,8 @@ impl Client {
|
|||
) -> Result<Vec<Score>, Error> {
|
||||
let mut r = UserScoreRequestBuilder::new(u, user);
|
||||
f(&mut r);
|
||||
let res = self.build_request(client, r.build(client))?.json()?;
|
||||
let res: Vec<raw::Score> = self.build_request(client, r.build(client))?.json()?;
|
||||
let res = vec_try_into(res)?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
use chrono::{DateTime, Duration, Utc};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod deser;
|
||||
pub mod mods;
|
||||
pub mod parse;
|
||||
pub(crate) mod raw;
|
||||
|
||||
pub use mods::Mods;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum ApprovalStatus {
|
||||
Loved,
|
||||
Qualified,
|
||||
|
@ -28,7 +30,7 @@ impl fmt::Display for ApprovalStatus {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Difficulty {
|
||||
pub stars: f64,
|
||||
pub aim: Option<f64>,
|
||||
|
@ -45,7 +47,7 @@ pub struct Difficulty {
|
|||
pub max_combo: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub enum Genre {
|
||||
Any,
|
||||
Unspecified,
|
||||
|
@ -70,7 +72,7 @@ impl fmt::Display for Genre {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum Language {
|
||||
Any,
|
||||
Other,
|
||||
|
@ -92,7 +94,7 @@ impl fmt::Display for Language {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Mode {
|
||||
Std,
|
||||
Taiko,
|
||||
|
@ -116,7 +118,7 @@ impl fmt::Display for Mode {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Beatmap {
|
||||
// Beatmapset info
|
||||
pub approval: ApprovalStatus,
|
||||
|
@ -169,7 +171,7 @@ impl Beatmap {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserEvent {
|
||||
pub display_html: String,
|
||||
pub beatmap_id: u64,
|
||||
|
@ -178,7 +180,7 @@ pub struct UserEvent {
|
|||
pub epic_factor: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: u64,
|
||||
pub username: String,
|
||||
|
@ -216,7 +218,7 @@ impl User {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Rank {
|
||||
SS,
|
||||
SSH,
|
||||
|
@ -253,7 +255,7 @@ impl fmt::Display for Rank {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Score {
|
||||
pub id: Option<u64>, // No id if you fail
|
||||
pub user_id: u64,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// The mods available to osu!
|
||||
#[derive(std::default::Default)]
|
||||
#[derive(std::default::Default, Serialize, Deserialize)]
|
||||
pub struct Mods: u64 {
|
||||
const NOMOD = 0;
|
||||
const NF = 1 << 0;
|
||||
|
|
|
@ -1,17 +1,43 @@
|
|||
use super::*;
|
||||
use chrono::{
|
||||
format::{parse, Item, Numeric, Pad, Parsed},
|
||||
DateTime, Duration, Utc,
|
||||
DateTime, ParseError as ChronoParseError, Utc,
|
||||
};
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
use std::str::FromStr;
|
||||
use std::convert::TryFrom;
|
||||
use std::time::Duration;
|
||||
use std::{error::Error, fmt, str::FromStr};
|
||||
|
||||
impl<'de> Deserialize<'de> for Score {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let raw: raw::Score = raw::Score::deserialize(deserializer)?;
|
||||
/// Errors that can be identified from parsing.
|
||||
#[derive(Debug)]
|
||||
pub enum ParseError {
|
||||
InvalidValue { field: &'static str, value: String },
|
||||
FromStr(String),
|
||||
NoApprovalDate,
|
||||
DateParseError(ChronoParseError),
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use ParseError::*;
|
||||
match self {
|
||||
InvalidValue {
|
||||
ref field,
|
||||
ref value,
|
||||
} => write!(f, "Invalid value `{}` for {}", value, field),
|
||||
FromStr(ref s) => write!(f, "Invalid value `{}` parsing from string", s),
|
||||
NoApprovalDate => write!(f, "Approval date expected but not found"),
|
||||
DateParseError(ref r) => write!(f, "Error parsing date: {}", r),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseError {}
|
||||
|
||||
type ParseResult<T> = Result<T, ParseError>;
|
||||
|
||||
impl TryFrom<raw::Score> for Score {
|
||||
type Error = ParseError;
|
||||
fn try_from(raw: raw::Score) -> Result<Self, Self::Error> {
|
||||
Ok(Score {
|
||||
id: raw.score_id.map(parse_from_str).transpose()?,
|
||||
user_id: parse_from_str(&raw.user_id)?,
|
||||
|
@ -41,12 +67,9 @@ impl<'de> Deserialize<'de> for Score {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for User {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let raw: raw::User = raw::User::deserialize(deserializer)?;
|
||||
impl TryFrom<raw::User> for User {
|
||||
type Error = ParseError;
|
||||
fn try_from(raw: raw::User) -> Result<Self, Self::Error> {
|
||||
Ok(User {
|
||||
id: parse_from_str(&raw.user_id)?,
|
||||
username: raw.username,
|
||||
|
@ -80,12 +103,9 @@ impl<'de> Deserialize<'de> for User {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Beatmap {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let raw: raw::Beatmap = raw::Beatmap::deserialize(deserializer)?;
|
||||
impl TryFrom<raw::Beatmap> for Beatmap {
|
||||
type Error = ParseError;
|
||||
fn try_from(raw: raw::Beatmap) -> Result<Self, Self::Error> {
|
||||
Ok(Beatmap {
|
||||
approval: parse_approval_status(&raw)?,
|
||||
submit_date: parse_date(&raw.submit_date)?,
|
||||
|
@ -129,7 +149,7 @@ impl<'de> Deserialize<'de> for Beatmap {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_user_event<E: de::Error>(s: raw::UserEvent) -> Result<UserEvent, E> {
|
||||
fn parse_user_event(s: raw::UserEvent) -> ParseResult<UserEvent> {
|
||||
Ok(UserEvent {
|
||||
display_html: s.display_html,
|
||||
beatmap_id: parse_from_str(&s.beatmap_id)?,
|
||||
|
@ -139,7 +159,7 @@ fn parse_user_event<E: de::Error>(s: raw::UserEvent) -> Result<UserEvent, E> {
|
|||
})
|
||||
}
|
||||
|
||||
fn parse_mode<E: de::Error>(s: impl AsRef<str>) -> Result<Mode, E> {
|
||||
fn parse_mode(s: impl AsRef<str>) -> ParseResult<Mode> {
|
||||
let t: u8 = parse_from_str(s)?;
|
||||
use Mode::*;
|
||||
Ok(match t {
|
||||
|
@ -147,11 +167,16 @@ fn parse_mode<E: de::Error>(s: impl AsRef<str>) -> Result<Mode, E> {
|
|||
1 => Taiko,
|
||||
2 => Catch,
|
||||
3 => Mania,
|
||||
_ => return Err(E::custom(format!("invalid value {} for mode", t))),
|
||||
_ => {
|
||||
return Err(ParseError::InvalidValue {
|
||||
field: "mode",
|
||||
value: t.to_string(),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_language<E: de::Error>(s: impl AsRef<str>) -> Result<Language, E> {
|
||||
fn parse_language(s: impl AsRef<str>) -> ParseResult<Language> {
|
||||
let t: u8 = parse_from_str(s)?;
|
||||
use Language::*;
|
||||
Ok(match t {
|
||||
|
@ -167,11 +192,16 @@ fn parse_language<E: de::Error>(s: impl AsRef<str>) -> Result<Language, E> {
|
|||
9 => Swedish,
|
||||
10 => Spanish,
|
||||
11 => Italian,
|
||||
_ => return Err(E::custom(format!("invalid value {} for language", t))),
|
||||
_ => {
|
||||
return Err(ParseError::InvalidValue {
|
||||
field: "langugae",
|
||||
value: t.to_string(),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_genre<E: de::Error>(s: impl AsRef<str>) -> Result<Genre, E> {
|
||||
fn parse_genre(s: impl AsRef<str>) -> ParseResult<Genre> {
|
||||
let t: u8 = parse_from_str(s)?;
|
||||
use Genre::*;
|
||||
Ok(match t {
|
||||
|
@ -185,45 +215,57 @@ fn parse_genre<E: de::Error>(s: impl AsRef<str>) -> Result<Genre, E> {
|
|||
7 => Novelty,
|
||||
9 => HipHop,
|
||||
10 => Electronic,
|
||||
_ => return Err(E::custom(format!("invalid value {} for genre", t))),
|
||||
_ => {
|
||||
return Err(ParseError::InvalidValue {
|
||||
field: "genre",
|
||||
value: t.to_string(),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_duration<E: de::Error>(s: impl AsRef<str>) -> Result<Duration, E> {
|
||||
Ok(Duration::seconds(parse_from_str(s)?))
|
||||
fn parse_duration(s: impl AsRef<str>) -> ParseResult<Duration> {
|
||||
Ok(Duration::from_secs(parse_from_str(s)?))
|
||||
}
|
||||
|
||||
fn parse_from_str<T: FromStr, E: de::Error>(s: impl AsRef<str>) -> Result<T, E> {
|
||||
T::from_str(s.as_ref()).map_err(|_| E::custom(format!("invalid value {}", s.as_ref())))
|
||||
fn parse_from_str<T: FromStr>(s: impl AsRef<str>) -> ParseResult<T> {
|
||||
let v = s.as_ref();
|
||||
T::from_str(v).map_err(|_| ParseError::FromStr(v.to_owned()))
|
||||
}
|
||||
|
||||
fn parse_bool<E: de::Error>(b: impl AsRef<str>) -> Result<bool, E> {
|
||||
fn parse_bool(b: impl AsRef<str>) -> ParseResult<bool> {
|
||||
match b.as_ref() {
|
||||
"1" => Ok(true),
|
||||
"0" => Ok(false),
|
||||
_ => Err(E::custom("invalid value for bool")),
|
||||
t => Err(ParseError::InvalidValue {
|
||||
field: "bool",
|
||||
value: t.to_owned(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_approval_status<E: de::Error>(b: &raw::Beatmap) -> Result<ApprovalStatus, E> {
|
||||
fn parse_approval_status(b: &raw::Beatmap) -> ParseResult<ApprovalStatus> {
|
||||
use ApprovalStatus::*;
|
||||
Ok(match &b.approved[..] {
|
||||
"4" => Loved,
|
||||
"3" => Qualified,
|
||||
"2" => Approved,
|
||||
"1" => Ranked(parse_date(
|
||||
b.approved_date
|
||||
.as_ref()
|
||||
.ok_or(E::custom("expected approved date got none"))?,
|
||||
b.approved_date.as_ref().ok_or(ParseError::NoApprovalDate)?,
|
||||
)?),
|
||||
"0" => Pending,
|
||||
"-1" => WIP,
|
||||
"-2" => Graveyarded,
|
||||
_ => return Err(E::custom("invalid value for approval status")),
|
||||
t => {
|
||||
return Err(ParseError::InvalidValue {
|
||||
field: "approval status",
|
||||
value: t.to_owned(),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_date<E: de::Error>(date: impl AsRef<str>) -> Result<DateTime<Utc>, E> {
|
||||
fn parse_date(date: impl AsRef<str>) -> ParseResult<DateTime<Utc>> {
|
||||
let mut parsed = Parsed::new();
|
||||
parse(
|
||||
&mut parsed,
|
||||
|
@ -243,6 +285,8 @@ fn parse_date<E: de::Error>(date: impl AsRef<str>) -> Result<DateTime<Utc>, E> {
|
|||
])
|
||||
.iter(),
|
||||
)
|
||||
.map_err(E::custom)?;
|
||||
parsed.to_datetime_with_timezone(&Utc {}).map_err(E::custom)
|
||||
.map_err(ParseError::DateParseError)?;
|
||||
parsed
|
||||
.to_datetime_with_timezone(&Utc {})
|
||||
.map_err(ParseError::DateParseError)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue