feat(v2): initial commit
This commit is contained in:
parent
48868983b3
commit
5a84558d89
44 changed files with 20091 additions and 3489 deletions
|
|
@ -1,460 +1,150 @@
|
|||
#![warn(clippy::all, clippy::perf, clippy::style, clippy::suspicious)]
|
||||
|
||||
pub mod into_owned;
|
||||
pub mod local;
|
||||
pub mod send;
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
pub mod user;
|
||||
|
||||
#[cfg(feature = "faction")]
|
||||
pub mod faction;
|
||||
|
||||
#[cfg(feature = "market")]
|
||||
pub mod market;
|
||||
|
||||
#[cfg(feature = "torn")]
|
||||
pub mod torn;
|
||||
|
||||
#[cfg(feature = "key")]
|
||||
pub mod key;
|
||||
|
||||
#[cfg(feature = "awc")]
|
||||
pub mod awc;
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub mod reqwest;
|
||||
|
||||
#[cfg(feature = "__common")]
|
||||
pub mod common;
|
||||
|
||||
mod de_util;
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{de::Error as DeError, Deserialize};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use into_owned::IntoOwned;
|
||||
pub mod executor;
|
||||
pub mod models;
|
||||
pub mod parameters;
|
||||
pub mod request;
|
||||
pub mod scopes;
|
||||
|
||||
pub struct ApiResponse {
|
||||
pub value: serde_json::Value,
|
||||
#[derive(Debug, Error, Clone, PartialEq, Eq)]
|
||||
pub enum ApiError {
|
||||
#[error("Unhandled error, should not occur")]
|
||||
Unknown,
|
||||
#[error("Private key is empty in current request")]
|
||||
KeyIsEmpty,
|
||||
#[error("Private key is wrong/incorrect format")]
|
||||
IncorrectKey,
|
||||
#[error("Requesting an incorrect basic type")]
|
||||
WrongType,
|
||||
#[error("Requesting incorect selection fields")]
|
||||
WrongFields,
|
||||
#[error(
|
||||
"Requests are blocked for a small period of time because of too many requests per user"
|
||||
)]
|
||||
TooManyRequest,
|
||||
#[error("Wrong ID value")]
|
||||
IncorrectId,
|
||||
#[error("A requested selection is private")]
|
||||
IncorrectIdEntityRelation,
|
||||
#[error("Current IP is banned for a small period of time because of abuse")]
|
||||
IpBlock,
|
||||
#[error("Api system is currently disabled")]
|
||||
ApiDisabled,
|
||||
#[error("Current key can't be used because owner is in federal jail")]
|
||||
KeyOwnerInFederalJail,
|
||||
#[error("You can only change your API key once every 60 seconds")]
|
||||
KeyChange,
|
||||
#[error("Error reading key from Database")]
|
||||
KeyRead,
|
||||
#[error("The key owner hasn't been online for more than 7 days")]
|
||||
TemporaryInactivity,
|
||||
#[error("Too many records have been pulled today by this user from our cloud services")]
|
||||
DailyReadLimit,
|
||||
#[error("An error code specifically for testing purposes that has no dedicated meaning")]
|
||||
TemporaryError,
|
||||
#[error("A selection is being called of which this key does not have permission to access")]
|
||||
InsufficientAccessLevel,
|
||||
#[error("Backend error occurred, please try again")]
|
||||
Backend,
|
||||
#[error("API key has been paused by the owner")]
|
||||
Paused,
|
||||
#[error("Must be migrated to crimes 2.0")]
|
||||
NotMigratedCrimes,
|
||||
#[error("Race not yet finished")]
|
||||
RaceNotFinished,
|
||||
#[error("Wrong cat value")]
|
||||
IncorrectCategory,
|
||||
#[error("This selection is only available in API v1")]
|
||||
OnlyInV1,
|
||||
#[error("This selection is only available in API v2")]
|
||||
OnlyInV2,
|
||||
#[error("Closed temporarily")]
|
||||
ClosedTemporarily,
|
||||
#[error("Other: {message}")]
|
||||
Other { code: u16, message: String },
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ResponseError {
|
||||
#[error("API: {reason}")]
|
||||
Api { code: u8, reason: String },
|
||||
impl ApiError {
|
||||
pub fn new(code: u16, message: &str) -> Self {
|
||||
match code {
|
||||
0 => Self::Unknown,
|
||||
1 => Self::KeyIsEmpty,
|
||||
2 => Self::IncorrectKey,
|
||||
3 => Self::WrongType,
|
||||
4 => Self::WrongFields,
|
||||
5 => Self::TooManyRequest,
|
||||
6 => Self::IncorrectId,
|
||||
7 => Self::IncorrectIdEntityRelation,
|
||||
8 => Self::IpBlock,
|
||||
9 => Self::ApiDisabled,
|
||||
10 => Self::KeyOwnerInFederalJail,
|
||||
11 => Self::KeyChange,
|
||||
12 => Self::KeyRead,
|
||||
13 => Self::TemporaryInactivity,
|
||||
14 => Self::DailyReadLimit,
|
||||
15 => Self::TemporaryError,
|
||||
16 => Self::InsufficientAccessLevel,
|
||||
17 => Self::Backend,
|
||||
18 => Self::Paused,
|
||||
19 => Self::NotMigratedCrimes,
|
||||
20 => Self::RaceNotFinished,
|
||||
21 => Self::IncorrectCategory,
|
||||
22 => Self::OnlyInV1,
|
||||
23 => Self::OnlyInV2,
|
||||
24 => Self::ClosedTemporarily,
|
||||
other => Self::Other {
|
||||
code: other,
|
||||
message: message.to_owned(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[error(transparent)]
|
||||
MalformedResponse(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl ResponseError {
|
||||
pub fn api_code(&self) -> Option<u8> {
|
||||
pub fn code(&self) -> u16 {
|
||||
match self {
|
||||
Self::Api { code, .. } => Some(*code),
|
||||
_ => None,
|
||||
Self::Unknown => 0,
|
||||
Self::KeyIsEmpty => 1,
|
||||
Self::IncorrectKey => 2,
|
||||
Self::WrongType => 3,
|
||||
Self::WrongFields => 4,
|
||||
Self::TooManyRequest => 5,
|
||||
Self::IncorrectId => 6,
|
||||
Self::IncorrectIdEntityRelation => 7,
|
||||
Self::IpBlock => 8,
|
||||
Self::ApiDisabled => 9,
|
||||
Self::KeyOwnerInFederalJail => 10,
|
||||
Self::KeyChange => 11,
|
||||
Self::KeyRead => 12,
|
||||
Self::TemporaryInactivity => 13,
|
||||
Self::DailyReadLimit => 14,
|
||||
Self::TemporaryError => 15,
|
||||
Self::InsufficientAccessLevel => 16,
|
||||
Self::Backend => 17,
|
||||
Self::Paused => 18,
|
||||
Self::NotMigratedCrimes => 19,
|
||||
Self::RaceNotFinished => 20,
|
||||
Self::IncorrectCategory => 21,
|
||||
Self::OnlyInV1 => 22,
|
||||
Self::OnlyInV2 => 23,
|
||||
Self::ClosedTemporarily => 24,
|
||||
Self::Other { code, .. } => *code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiResponse {
|
||||
pub fn from_value(mut value: serde_json::Value) -> Result<Self, ResponseError> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ApiErrorDto {
|
||||
code: u8,
|
||||
#[serde(rename = "error")]
|
||||
reason: String,
|
||||
}
|
||||
match value.get_mut("error") {
|
||||
Some(error) => {
|
||||
let dto: ApiErrorDto = serde_json::from_value(error.take())?;
|
||||
Err(ResponseError::Api {
|
||||
code: dto.code,
|
||||
reason: dto.reason,
|
||||
})
|
||||
}
|
||||
None => Ok(Self { value }),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn decode<'de, D>(&'de self) -> serde_json::Result<D>
|
||||
where
|
||||
D: Deserialize<'de>,
|
||||
{
|
||||
D::deserialize(&self.value)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn decode_field<'de, D>(&'de self, field: &'static str) -> serde_json::Result<D>
|
||||
where
|
||||
D: Deserialize<'de>,
|
||||
{
|
||||
self.value
|
||||
.get(field)
|
||||
.ok_or_else(|| serde_json::Error::missing_field(field))
|
||||
.and_then(D::deserialize)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn decode_field_with<'de, V, F>(&'de self, field: &'static str, fun: F) -> serde_json::Result<V>
|
||||
where
|
||||
F: FnOnce(&'de serde_json::Value) -> serde_json::Result<V>,
|
||||
{
|
||||
self.value
|
||||
.get(field)
|
||||
.ok_or_else(|| serde_json::Error::missing_field(field))
|
||||
.and_then(fun)
|
||||
}
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum ParameterError {
|
||||
#[error("value `{value}` is out of range for parameter {name}")]
|
||||
OutOfRange { name: &'static str, value: i32 },
|
||||
}
|
||||
|
||||
pub trait ApiSelectionResponse: Send + Sync + From<ApiResponse> + 'static {
|
||||
fn into_inner(self) -> ApiResponse;
|
||||
}
|
||||
|
||||
pub trait ApiSelection: Send + Sync + 'static {
|
||||
type Response: ApiSelectionResponse;
|
||||
|
||||
fn raw_value(self) -> &'static str;
|
||||
|
||||
fn category() -> &'static str;
|
||||
}
|
||||
|
||||
pub struct DirectExecutor<C> {
|
||||
key: String,
|
||||
_marker: std::marker::PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C> DirectExecutor<C> {
|
||||
fn new(key: String) -> Self {
|
||||
Self {
|
||||
key,
|
||||
_marker: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ApiClientError<C>
|
||||
where
|
||||
C: std::error::Error,
|
||||
{
|
||||
#[error(transparent)]
|
||||
Client(C),
|
||||
|
||||
#[error(transparent)]
|
||||
Response(#[from] ResponseError),
|
||||
}
|
||||
|
||||
impl<C> ApiClientError<C>
|
||||
where
|
||||
C: std::error::Error,
|
||||
{
|
||||
pub fn api_code(&self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Response(err) => err.api_code(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApiRequest<A>
|
||||
where
|
||||
A: ApiSelection,
|
||||
{
|
||||
pub selections: Vec<&'static str>,
|
||||
pub query_items: Vec<(&'static str, String)>,
|
||||
pub comment: Option<String>,
|
||||
phantom: std::marker::PhantomData<A>,
|
||||
}
|
||||
|
||||
impl<A> std::default::Default for ApiRequest<A>
|
||||
where
|
||||
A: ApiSelection,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
selections: Vec::default(),
|
||||
query_items: Vec::default(),
|
||||
comment: None,
|
||||
phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> ApiRequest<A>
|
||||
where
|
||||
A: ApiSelection,
|
||||
{
|
||||
fn add_query_item(&mut self, name: &'static str, value: impl ToString) {
|
||||
if let Some((_, old)) = self.query_items.iter_mut().find(|(n, _)| *n == name) {
|
||||
*old = value.to_string();
|
||||
} else {
|
||||
self.query_items.push((name, value.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url(&self, key: &str, id: Option<&str>) -> String {
|
||||
let mut url = format!("https://api.torn.com/{}/", A::category());
|
||||
|
||||
if let Some(id) = id {
|
||||
write!(url, "{}", id).unwrap();
|
||||
}
|
||||
|
||||
write!(url, "?selections={}&key={}", self.selections.join(","), key).unwrap();
|
||||
|
||||
for (name, value) in &self.query_items {
|
||||
write!(url, "&{name}={value}").unwrap();
|
||||
}
|
||||
|
||||
if let Some(comment) = &self.comment {
|
||||
write!(url, "&comment={}", comment).unwrap();
|
||||
}
|
||||
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ApiRequestBuilder<A>
|
||||
where
|
||||
A: ApiSelection,
|
||||
{
|
||||
pub request: ApiRequest<A>,
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
impl<A> Default for ApiRequestBuilder<A>
|
||||
where
|
||||
A: ApiSelection,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
request: Default::default(),
|
||||
id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> ApiRequestBuilder<A>
|
||||
where
|
||||
A: ApiSelection,
|
||||
{
|
||||
#[must_use]
|
||||
pub fn selections(mut self, selections: impl IntoIterator<Item = A>) -> Self {
|
||||
self.request.selections.append(
|
||||
&mut selections
|
||||
.into_iter()
|
||||
.map(ApiSelection::raw_value)
|
||||
.collect(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from(mut self, from: DateTime<Utc>) -> Self {
|
||||
self.request.add_query_item("from", from.timestamp());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_timestamp(mut self, from: i64) -> Self {
|
||||
self.request.add_query_item("from", from);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to(mut self, to: DateTime<Utc>) -> Self {
|
||||
self.request.add_query_item("to", to.timestamp());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_timestamp(mut self, to: i64) -> Self {
|
||||
self.request.add_query_item("to", to);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn stats_timestamp(mut self, ts: i64) -> Self {
|
||||
self.request.add_query_item("timestamp", ts);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn stats_datetime(mut self, dt: DateTime<Utc>) -> Self {
|
||||
self.request.add_query_item("timestamp", dt.timestamp());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn comment(mut self, comment: String) -> Self {
|
||||
self.request.comment = Some(comment);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn id<I>(mut self, id: I) -> Self
|
||||
where
|
||||
I: ToString,
|
||||
{
|
||||
self.id = Some(id.to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(unused)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Once;
|
||||
|
||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
||||
pub use ::awc::Client;
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub use ::reqwest::Client;
|
||||
|
||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
||||
pub use crate::local::ApiClient as ClientTrait;
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub use crate::send::ApiClient as ClientTrait;
|
||||
|
||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
||||
pub use actix_rt::test as async_test;
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub use tokio::test as async_test;
|
||||
|
||||
use super::*;
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
pub(crate) fn setup() -> String {
|
||||
INIT.call_once(|| {
|
||||
dotenv::dotenv().ok();
|
||||
});
|
||||
std::env::var("APIKEY").expect("api key")
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
#[test]
|
||||
fn selection_raw_value() {
|
||||
assert_eq!(user::Selection::Basic.raw_value(), "basic");
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "reqwest", feature = "user"))]
|
||||
#[tokio::test]
|
||||
async fn reqwest() {
|
||||
let key = setup();
|
||||
|
||||
Client::default().torn_api(key).user(|b| b).await.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "awc", feature = "user"))]
|
||||
#[actix_rt::test]
|
||||
async fn awc() {
|
||||
let key = setup();
|
||||
|
||||
Client::default().torn_api(key).user(|b| b).await.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_builder_from_dt() {
|
||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
||||
.from(DateTime::default())
|
||||
.request
|
||||
.url("", None);
|
||||
|
||||
assert_eq!("https://api.torn.com/user/?selections=&key=&from=0", url);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_builder_from_ts() {
|
||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
||||
.from_timestamp(12345)
|
||||
.request
|
||||
.url("", None);
|
||||
|
||||
assert_eq!(
|
||||
"https://api.torn.com/user/?selections=&key=&from=12345",
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_builder_to_dt() {
|
||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
||||
.to(DateTime::default())
|
||||
.request
|
||||
.url("", None);
|
||||
|
||||
assert_eq!("https://api.torn.com/user/?selections=&key=&to=0", url);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_builder_to_ts() {
|
||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
||||
.to_timestamp(12345)
|
||||
.request
|
||||
.url("", None);
|
||||
|
||||
assert_eq!("https://api.torn.com/user/?selections=&key=&to=12345", url);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_builder_timestamp_dt() {
|
||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
||||
.stats_datetime(DateTime::default())
|
||||
.request
|
||||
.url("", None);
|
||||
|
||||
assert_eq!(
|
||||
"https://api.torn.com/user/?selections=&key=×tamp=0",
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_builder_timestamp_ts() {
|
||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
||||
.stats_timestamp(12345)
|
||||
.request
|
||||
.url("", None);
|
||||
|
||||
assert_eq!(
|
||||
"https://api.torn.com/user/?selections=&key=×tamp=12345",
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_builder_duplicate() {
|
||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
||||
.from(DateTime::default())
|
||||
.from_timestamp(12345)
|
||||
.request
|
||||
.url("", None);
|
||||
|
||||
assert_eq!(
|
||||
"https://api.torn.com/user/?selections=&key=&from=12345",
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_builder_many_options() {
|
||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
||||
.from(DateTime::default())
|
||||
.to_timestamp(60)
|
||||
.stats_timestamp(12345)
|
||||
.selections([user::Selection::PersonalStats])
|
||||
.request
|
||||
.url("KEY", Some("1"));
|
||||
|
||||
assert_eq!(
|
||||
"https://api.torn.com/user/1?selections=personalstats&key=KEY&from=0&to=60×tamp=12345",
|
||||
url
|
||||
);
|
||||
}
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Parameter error: {0}")]
|
||||
Parameter(#[from] ParameterError),
|
||||
#[error("Network error: {0}")]
|
||||
Network(#[from] reqwest::Error),
|
||||
#[error("Parsing error: {0}")]
|
||||
Parsing(#[from] serde_json::Error),
|
||||
#[error("Api error: {0}")]
|
||||
Api(#[from] ApiError),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue