270 lines
9.8 KiB
Rust
270 lines
9.8 KiB
Rust
#![forbid(unsafe_code)]
|
|
//! <h1 align="center">torn-api.rs</h1>
|
|
//! <div align="center">
|
|
//! <strong>
|
|
//! Rust Torn API bindings
|
|
//! </strong>
|
|
//! </div>
|
|
//!
|
|
//! <br />
|
|
//!
|
|
//! <div align="center">
|
|
//! <!-- Version -->
|
|
//! <a href="https://crates.io/crates/torn-api">
|
|
//! <img src="https://img.shields.io/crates/v/torn-api.svg?style=flat-square"
|
|
//! alt="Crates.io version" /></a>
|
|
//! <!-- Docs -->
|
|
//! <a href="https://docs.rs/torn-api">
|
|
//! <img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square" alt="docs.rs docs" />
|
|
//! </a>
|
|
//! <!-- License -->
|
|
//! <img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license" />
|
|
//! </div>
|
|
//!
|
|
//! <br />
|
|
//!
|
|
//! Async and typesafe bindings for the [Torn API](https://www.torn.com/swagger.php) that are auto-generated based on the v2 OpenAPI spec.
|
|
//!
|
|
//! ## Installation
|
|
//! torn-api requires an async runtime such as [tokio](https://github.com/tokio-rs/tokio) or [smol](https://github.com/smol-rs/smol) in order to function. It *should* be fully runtime agnostic when the `reqwest` feature is disabled.
|
|
//! ```toml
|
|
//! [dependencies]
|
|
//! torn-api = "1.7"
|
|
//! ```
|
|
//!
|
|
//! ### Features
|
|
//! - `reqwest`: Include an implementation of the client which uses the [reqwest](https://github.com/seanmonstar/reqwest) crate as its HTTP client. Requires tokio runtime.
|
|
//! - `models`: Generate response and parameter model definitions.
|
|
//! - `requests`: Generate requests model definitions.
|
|
//! - `scopes`: Generate scope objects which group endpoints by category.
|
|
//! - `builder`: Generate builders using [bon](https://github.com/elastio/bon) for all request structs.
|
|
//! - `strum`: Derive [EnumIs](https://docs.rs/strum/latest/strum/derive.EnumIs.html) and [EnumTryAs](https://docs.rs/strum/latest/strum/derive.EnumTryAs.html) for all auto-generated enums.
|
|
//!
|
|
//! ## Quickstart
|
|
//!
|
|
//! ```rust,no_run
|
|
//! use torn_api::{executor::{ReqwestClient, ExecutorExt}, models::RacingRaceTypeEnum};
|
|
//! # #[tokio::main]
|
|
//! # async fn main() {
|
|
//! let client = ReqwestClient::new("XXXXXXXXXXXXX");
|
|
//!
|
|
//! let response = client.user().races(|r| r.cat(RacingRaceTypeEnum::Official)).await.unwrap();
|
|
//!
|
|
//! let race = &response.races[0];
|
|
//!
|
|
//! println!("Race '{}': winner was {}", race.title, race.results[0].driver_id);
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! ### Use with undocumented endpoints
|
|
//! The v2 API exposes v1 endpoints as undocumented endpoints in cases where they haven't been ported over yet. It is still possible (though not recommended) to use this crate with such endpoints by manually implementing the [`IntoRequest`](https://docs.rs/torn-api/latest/torn_api/request/trait.IntoRequest.html) trait.
|
|
//!
|
|
//!
|
|
//! ```rust,no_run
|
|
//! use torn_api::{
|
|
//! executor::{ReqwestClient, Executor},
|
|
//! models::UserId,
|
|
//! request::{IntoRequest, ApiRequest}
|
|
//! };
|
|
//!
|
|
//! #[derive(serde::Deserialize)]
|
|
//! struct UserBasic {
|
|
//! id: UserId,
|
|
//! name: String,
|
|
//! level: i32
|
|
//! }
|
|
//!
|
|
//! struct UserBasicRequest(UserId);
|
|
//!
|
|
//! impl IntoRequest for UserBasicRequest {
|
|
//! type Discriminant = UserId;
|
|
//! type Response = UserBasic;
|
|
//! fn into_request(self) -> (Self::Discriminant, ApiRequest) {
|
|
//! let request = ApiRequest {
|
|
//! path: format!("/user/{}/basic", self.0),
|
|
//! parameters: Vec::default(),
|
|
//! };
|
|
//!
|
|
//! (self.0, request)
|
|
//! }
|
|
//! }
|
|
//!
|
|
//! # #[tokio::main]
|
|
//! # async fn main() {
|
|
//! let client = ReqwestClient::new("XXXXXXXXXXXXX");
|
|
//! let basic = client.fetch(UserBasicRequest(UserId(1))).await.unwrap();
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! ### Implementing your own API executor
|
|
//! If you don't wish to use reqwest, or want to use custom logic for which API key to use, you have to implement the [`Executor`](https://docs.rs/torn-api/latest/torn_api/executor/trait.Executor.html) trait for your custom executor.
|
|
//!
|
|
//! ## Safety
|
|
//! The crate is compiled with `#![forbid(unsafe_code)]`.
|
|
//!
|
|
//! ## Warnings
|
|
//! - ⚠️ The Torn v2 API, on which this wrapper is based, is under active development and changes frequently. No guarantees are made that this wrapper always matches the latest version of the API.
|
|
//! - ⚠️ This crate contains a lot of macro-heavy, auto-generated code. If you experience slow compile times, you may want try testing the [nightly only `-Zhint-mostly-unused` option](https://blog.rust-lang.org/inside-rust/2025/07/15/call-for-testing-hint-mostly-unused) to see if improvements in compile time apply to your use case.
|
|
|
|
use thiserror::Error;
|
|
|
|
/// Traits to execute api requests
|
|
pub mod executor;
|
|
#[cfg(feature = "models")]
|
|
/// Auto-generated model definitions.
|
|
pub mod models;
|
|
#[cfg(feature = "requests")]
|
|
/// Auto-generated parameter definitions.
|
|
pub mod parameters;
|
|
/// Api request traits and auto-generated definitions.
|
|
pub mod request;
|
|
#[cfg(feature = "scopes")]
|
|
/// Auto-generated api categories for convenient access.
|
|
pub mod scopes;
|
|
|
|
/// Error returned by the API
|
|
#[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 },
|
|
}
|
|
|
|
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(),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn code(&self) -> u16 {
|
|
match self {
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Error for invalid parameter values
|
|
#[derive(Debug, Error, PartialEq, Eq)]
|
|
pub enum ParameterError {
|
|
#[error("value `{value}` is out of range for parameter {name}")]
|
|
OutOfRange { name: &'static str, value: i32 },
|
|
}
|
|
|
|
/// Error returned by the default Executor
|
|
#[derive(Debug, Error)]
|
|
pub enum Error {
|
|
#[error("Parameter error: {0}")]
|
|
Parameter(#[from] ParameterError),
|
|
#[cfg(feature = "reqwest")]
|
|
#[error("Network error: {0}")]
|
|
Network(#[from] reqwest::Error),
|
|
#[error("Parsing error: {0}")]
|
|
Parsing(#[from] serde_json::Error),
|
|
#[error("Api error: {0}")]
|
|
Api(#[from] ApiError),
|
|
}
|