feat(nexusmods): Start API implementation
This commit is contained in:
parent
13d36c4947
commit
5ca1ca3506
2 changed files with 151 additions and 0 deletions
19
lib/nexusmods/Cargo.toml
Normal file
19
lib/nexusmods/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "nexusmods"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
reqwest = { version = "0.11.14" }
|
||||||
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
|
serde_json = "1.0.94"
|
||||||
|
thiserror = "1.0.39"
|
||||||
|
time = { version = "0.3.20", features = ["serde"] }
|
||||||
|
tracing = "0.1.37"
|
||||||
|
url = "2.3.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.26.0", features = ["rt", "macros"] }
|
132
lib/nexusmods/src/lib.rs
Normal file
132
lib/nexusmods/src/lib.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue};
|
||||||
|
use reqwest::{Client, Url};
|
||||||
|
use serde::ser::SerializeTuple;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
// TODO: Add OS information
|
||||||
|
const USER_AGENT: &str = concat!("DTMM/", env!("CARGO_PKG_VERSION"));
|
||||||
|
const GAME_ID: &str = "warhammer40kdarktide";
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref BASE_URL: Url = Url::parse("https://api.nexusmods.com/v1/").unwrap();
|
||||||
|
static ref BASE_URL_GAME: Url =
|
||||||
|
Url::parse("https://api.nexusmods.com/v1/games/warhammer40kdarktide/").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("HTTP error: {0:?}")]
|
||||||
|
HTTP(#[from] reqwest::Error),
|
||||||
|
#[error("invalid URL: {0:?}")]
|
||||||
|
URLParseError(#[from] url::ParseError),
|
||||||
|
#[error("failed to deserialize '{error}': {json}")]
|
||||||
|
Deserialize {
|
||||||
|
json: String,
|
||||||
|
error: serde_json::Error,
|
||||||
|
},
|
||||||
|
#[error(transparent)]
|
||||||
|
InvalidHeaderValue(#[from] InvalidHeaderValue),
|
||||||
|
#[error("this error cannot happen")]
|
||||||
|
Infallible(#[from] Infallible),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct UpdateInfo {
|
||||||
|
pub mod_id: u64,
|
||||||
|
#[serde(with = "time::serde::timestamp")]
|
||||||
|
pub latest_file_update: OffsetDateTime,
|
||||||
|
#[serde(with = "time::serde::timestamp")]
|
||||||
|
pub latest_mod_activity: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum UpdatePeriod {
|
||||||
|
Day,
|
||||||
|
Week,
|
||||||
|
Month,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UpdatePeriod {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Week
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for UpdatePeriod {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let mut tup = serializer.serialize_tuple(2)?;
|
||||||
|
tup.serialize_element("period")?;
|
||||||
|
tup.serialize_element(match self {
|
||||||
|
Self::Day => "1d",
|
||||||
|
Self::Week => "1w",
|
||||||
|
Self::Month => "1m",
|
||||||
|
})?;
|
||||||
|
tup.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Api {
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Api {
|
||||||
|
pub fn new(key: String) -> Result<Self> {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("accept", HeaderValue::from_static("application/json"));
|
||||||
|
headers.insert("apikey", HeaderValue::from_str(&key)?);
|
||||||
|
|
||||||
|
let client = Client::builder()
|
||||||
|
.user_agent(USER_AGENT)
|
||||||
|
.default_headers(headers)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
Ok(Self { client })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn mods_updated(&self, period: UpdatePeriod) -> Result<Vec<UpdateInfo>> {
|
||||||
|
let url = BASE_URL_GAME.join("mods/updated.json")?;
|
||||||
|
|
||||||
|
let res = self
|
||||||
|
.client
|
||||||
|
.get(url)
|
||||||
|
.query(&[period])
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
tracing::trace!(?res);
|
||||||
|
let json = res.text().await?;
|
||||||
|
|
||||||
|
serde_json::from_str(&json).map_err(|error| Error::Deserialize { json, error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::Api;
|
||||||
|
|
||||||
|
fn make_api() -> Api {
|
||||||
|
let key = std::env::var("NEXUSMODS_API_KEY").expect("'NEXUSMODS_API_KEY' env var missing");
|
||||||
|
Api::new(key).expect("failed to build API client")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mods_updated() {
|
||||||
|
let client = make_api();
|
||||||
|
client
|
||||||
|
.mods_updated(Default::default())
|
||||||
|
.await
|
||||||
|
.expect("failed to query 'mods_updated'");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue