use std::convert::TryInto;
use std::sync::Arc;
use ::reqwest::{Client, ClientBuilder, Proxy};
use http::header::USER_AGENT;
use http::header::{HeaderMap, HeaderValue};
use crate::auth::{Auth, Credentials, Token};
use crate::download::{DownloadAction, Downloader};
use crate::error::{self, Error, Result};
use crate::games::{GameRef, Games};
use crate::mods::ModRef;
use crate::reports::Reports;
use crate::request::RequestBuilder;
use crate::routing::Route;
use crate::user::Me;
use crate::{TargetPlatform, TargetPortal};
const DEFAULT_HOST: &str = "https://api.mod.io/v1";
const TEST_HOST: &str = "https://api.test.mod.io/v1";
const DEFAULT_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), '/', env!("CARGO_PKG_VERSION"));
#[must_use]
pub struct Builder {
config: Config,
}
impl TargetPlatform {
#[inline]
fn header_name() -> &'static str {
"X-Modio-Platform"
}
fn into_header_value(self) -> HeaderValue {
match self {
Self::Android => HeaderValue::from_static("Android"),
Self::Ios => HeaderValue::from_static("iOS"),
Self::Linux => HeaderValue::from_static("Linux"),
Self::Mac => HeaderValue::from_static("Mac"),
Self::Windows => HeaderValue::from_static("Windows"),
Self::PS4 => HeaderValue::from_static("PS4"),
Self::PS5 => HeaderValue::from_static("PS5"),
Self::Switch => HeaderValue::from_static("Switch"),
Self::XboxOne => HeaderValue::from_static("XboxOne"),
Self::XboxSeriesX => HeaderValue::from_static("XboxSeriesX"),
Self::Oculus => HeaderValue::from_static("Oculus"),
}
}
}
impl TargetPortal {
#[inline]
fn header_name() -> &'static str {
"X-Modio-Portal"
}
fn into_header_value(self) -> HeaderValue {
match self {
Self::Steam => HeaderValue::from_static("Steam"),
Self::GOG => HeaderValue::from_static("GOG"),
Self::EGS => HeaderValue::from_static("EGS"),
Self::Itchio => HeaderValue::from_static("Itchio"),
Self::Nintendo => HeaderValue::from_static("Nintendo"),
Self::PSN => HeaderValue::from_static("PSN"),
Self::XboxLive => HeaderValue::from_static("XboxLive"),
Self::Apple => HeaderValue::from_static("Apple"),
Self::Google => HeaderValue::from_static("Google"),
Self::Facebook => HeaderValue::from_static("Facebook"),
Self::Discord => HeaderValue::from_static("Discord"),
}
}
}
struct Config {
host: Option<String>,
credentials: Credentials,
builder: Option<ClientBuilder>,
headers: HeaderMap,
proxies: Vec<Proxy>,
#[cfg(feature = "__tls")]
tls: TlsBackend,
error: Option<Error>,
}
#[cfg(feature = "__tls")]
enum TlsBackend {
#[cfg(feature = "default-tls")]
Default,
#[cfg(feature = "rustls-tls")]
Rustls,
}
#[cfg(feature = "__tls")]
impl Default for TlsBackend {
fn default() -> TlsBackend {
#[cfg(feature = "default-tls")]
{
TlsBackend::Default
}
#[cfg(all(feature = "rustls-tls", not(feature = "default-tls")))]
{
TlsBackend::Rustls
}
}
}
impl Builder {
pub fn new<C: Into<Credentials>>(credentials: C) -> Builder {
Builder {
config: Config {
host: None,
credentials: credentials.into(),
builder: None,
headers: HeaderMap::new(),
proxies: Vec::new(),
#[cfg(feature = "__tls")]
tls: TlsBackend::default(),
error: None,
},
}
}
pub fn build(self) -> Result<Modio> {
let config = self.config;
if let Some(e) = config.error {
return Err(e);
}
let host = config.host.unwrap_or_else(|| DEFAULT_HOST.to_string());
let credentials = config.credentials;
let client = {
let mut builder = {
let builder = config.builder.unwrap_or_else(Client::builder);
#[cfg(feature = "__tls")]
match config.tls {
#[cfg(feature = "default-tls")]
TlsBackend::Default => builder.use_native_tls(),
#[cfg(feature = "rustls-tls")]
TlsBackend::Rustls => builder.use_rustls_tls(),
}
#[cfg(not(feature = "__tls"))]
builder
};
let mut headers = config.headers;
if !headers.contains_key(USER_AGENT) {
headers.insert(USER_AGENT, HeaderValue::from_static(DEFAULT_AGENT));
}
for proxy in config.proxies {
builder = builder.proxy(proxy);
}
builder
.default_headers(headers)
.build()
.map_err(error::builder)?
};
Ok(Modio {
inner: Arc::new(ClientRef {
host,
client,
credentials,
}),
})
}
pub fn client<F>(mut self, f: F) -> Builder
where
F: FnOnce(ClientBuilder) -> ClientBuilder,
{
self.config.builder = Some(f(Client::builder()));
self
}
pub fn host<S: Into<String>>(mut self, host: S) -> Builder {
self.config.host = Some(host.into());
self
}
pub fn use_test(mut self) -> Builder {
self.config.host = Some(TEST_HOST.into());
self
}
pub fn user_agent<V>(mut self, value: V) -> Builder
where
V: TryInto<HeaderValue>,
V::Error: Into<http::Error>,
{
match value.try_into() {
Ok(value) => {
self.config.headers.insert(USER_AGENT, value);
}
Err(e) => {
self.config.error = Some(error::builder(e.into()));
}
}
self
}
pub fn proxy(mut self, proxy: Proxy) -> Builder {
self.config.proxies.push(proxy);
self
}
pub fn target_platform(mut self, platform: TargetPlatform) -> Builder {
let name = TargetPlatform::header_name();
let value = platform.into_header_value();
self.config.headers.insert(name, value);
self
}
pub fn target_portal(mut self, portal: TargetPortal) -> Builder {
let name = TargetPortal::header_name();
let value = portal.into_header_value();
self.config.headers.insert(name, value);
self
}
#[cfg(feature = "default-tls")]
pub fn use_default_tls(mut self) -> Builder {
self.config.tls = TlsBackend::Default;
self
}
#[cfg(feature = "rustls-tls")]
pub fn use_rustls_tls(mut self) -> Builder {
self.config.tls = TlsBackend::Rustls;
self
}
}
#[derive(Clone, Debug)]
pub struct Modio {
pub(crate) inner: Arc<ClientRef>,
}
#[derive(Debug)]
pub(crate) struct ClientRef {
pub(crate) host: String,
pub(crate) client: Client,
pub(crate) credentials: Credentials,
}
impl Modio {
pub fn builder<C: Into<Credentials>>(credentials: C) -> Builder {
Builder::new(credentials)
}
pub fn new<C>(credentials: C) -> Result<Self>
where
C: Into<Credentials>,
{
Builder::new(credentials).build()
}
pub fn host<H, C>(host: H, credentials: C) -> Result<Self>
where
H: Into<String>,
C: Into<Credentials>,
{
Builder::new(credentials).host(host).build()
}
#[must_use]
pub fn with_credentials<CR>(&self, credentials: CR) -> Self
where
CR: Into<Credentials>,
{
Self {
inner: Arc::new(ClientRef {
host: self.inner.host.clone(),
client: self.inner.client.clone(),
credentials: credentials.into(),
}),
}
}
#[must_use]
pub fn with_token<T>(&self, token: T) -> Self
where
T: Into<Token>,
{
Self {
inner: Arc::new(ClientRef {
host: self.inner.host.clone(),
client: self.inner.client.clone(),
credentials: Credentials {
api_key: self.inner.credentials.api_key.clone(),
token: Some(token.into()),
},
}),
}
}
pub fn auth(&self) -> Auth {
Auth::new(self.clone())
}
pub fn games(&self) -> Games {
Games::new(self.clone())
}
pub fn game(&self, game_id: u32) -> GameRef {
GameRef::new(self.clone(), game_id)
}
pub fn mod_(&self, game_id: u32, mod_id: u32) -> ModRef {
ModRef::new(self.clone(), game_id, mod_id)
}
pub fn download<A>(&self, action: A) -> Downloader
where
DownloadAction: From<A>,
{
Downloader::new(self.clone(), action.into())
}
pub fn user(&self) -> Me {
Me::new(self.clone())
}
pub fn reports(&self) -> Reports {
Reports::new(self.clone())
}
pub(crate) fn request(&self, route: Route) -> RequestBuilder {
RequestBuilder::new(self.clone(), route)
}
}