torn_api/
lib.rs

1#![forbid(unsafe_code)]
2//! <h1 align="center">torn-api.rs</h1>
3//! <div align="center">
4//!  <strong>
5//!    Rust Torn API bindings
6//!  </strong>
7//! </div>
8//!
9//! <br />
10//!
11//! <div align="center">
12//!   <!-- Version -->
13//!   <a href="https://crates.io/crates/torn-api">
14//!     <img src="https://img.shields.io/crates/v/torn-api.svg?style=flat-square"
15//!     alt="Crates.io version" /></a>
16//!   <!-- Docs -->
17//!   <a href="https://docs.rs/torn-api">
18//!     <img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square" alt="docs.rs docs" />
19//!   </a>
20//!   <!-- License -->
21//!   <img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license" />
22//! </div>
23//!
24//! <br />
25//!
26//! Async and typesafe bindings for the [Torn API](https://www.torn.com/swagger.php) that are auto-generated based on the v2 OpenAPI spec.
27//!
28//! ## Installation
29//! 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.
30//! ```toml
31//! [dependencies]
32//! torn-api = "1.7"
33//! ```
34//!
35//! ### Features
36//! - `reqwest`: Include an implementation of the client which uses the [reqwest](https://github.com/seanmonstar/reqwest) crate as its HTTP client. Requires tokio runtime.
37//! - `models`: Generate response and parameter model definitions.
38//! - `requests`: Generate requests model definitions.
39//! - `scopes`: Generate scope objects which group endpoints by category.
40//! - `builder`: Generate builders using [bon](https://github.com/elastio/bon) for all request structs.
41//! - `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.
42//!
43//! ## Quickstart
44//!
45//! ```rust,no_run
46//! use torn_api::{executor::{ReqwestClient, ExecutorExt}, models::RacingRaceTypeEnum};
47//! # #[tokio::main]
48//! # async fn main() {
49//! let client = ReqwestClient::new("XXXXXXXXXXXXX");
50//!
51//! let response = client.user().races(|r| r.cat(RacingRaceTypeEnum::Official)).await.unwrap();
52//!
53//! let race = &response.races[0];
54//!
55//! println!("Race '{}': winner was {}", race.title, race.results[0].driver_id);
56//! # }
57//! ```
58//!
59//! ### Use with undocumented endpoints
60//! 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.
61//!
62//!
63//! ```rust,no_run
64//! use torn_api::{
65//!     executor::{ReqwestClient, Executor},
66//!     models::UserId,
67//!     request::{IntoRequest, ApiRequest}
68//! };
69//!
70//! #[derive(serde::Deserialize)]
71//! struct UserBasic {
72//!     id: UserId,
73//!     name: String,
74//!     level: i32
75//! }
76//!
77//! struct UserBasicRequest(UserId);
78//!
79//! impl IntoRequest for UserBasicRequest {
80//!     type Discriminant = UserId;
81//!     type Response = UserBasic;
82//!     fn into_request(self) -> (Self::Discriminant, ApiRequest) {
83//!         let request = ApiRequest {
84//!             path: format!("/user/{}/basic", self.0),
85//!             parameters: Vec::default(),
86//!         };
87//!
88//!         (self.0, request)
89//!     }
90//! }
91//!
92//! # #[tokio::main]
93//! # async fn main() {
94//! let client = ReqwestClient::new("XXXXXXXXXXXXX");
95//! let basic = client.fetch(UserBasicRequest(UserId(1))).await.unwrap();
96//! # }
97//! ```
98//!
99//! ### Implementing your own API executor
100//! 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.
101//!
102//! ## Safety
103//! The crate is compiled with `#![forbid(unsafe_code)]`.
104//!
105//! ## Warnings
106//! - ⚠️ 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.
107//! - ⚠️ 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.
108
109use thiserror::Error;
110
111/// Traits to execute api requests
112pub mod executor;
113#[cfg(feature = "models")]
114/// Auto-generated model definitions.
115pub mod models;
116#[cfg(feature = "requests")]
117/// Auto-generated parameter definitions.
118pub mod parameters;
119/// Api request traits and auto-generated definitions.
120pub mod request;
121#[cfg(feature = "scopes")]
122/// Auto-generated api categories for convenient access.
123pub mod scopes;
124
125/// Error returned by the API
126#[derive(Debug, Error, Clone, PartialEq, Eq)]
127pub enum ApiError {
128    #[error("Unhandled error, should not occur")]
129    Unknown,
130    #[error("Private key is empty in current request")]
131    KeyIsEmpty,
132    #[error("Private key is wrong/incorrect format")]
133    IncorrectKey,
134    #[error("Requesting an incorrect basic type")]
135    WrongType,
136    #[error("Requesting incorect selection fields")]
137    WrongFields,
138    #[error(
139        "Requests are blocked for a small period of time because of too many requests per user"
140    )]
141    TooManyRequest,
142    #[error("Wrong ID value")]
143    IncorrectId,
144    #[error("A requested selection is private")]
145    IncorrectIdEntityRelation,
146    #[error("Current IP is banned for a small period of time because of abuse")]
147    IpBlock,
148    #[error("Api system is currently disabled")]
149    ApiDisabled,
150    #[error("Current key can't be used because owner is in federal jail")]
151    KeyOwnerInFederalJail,
152    #[error("You can only change your API key once every 60 seconds")]
153    KeyChange,
154    #[error("Error reading key from Database")]
155    KeyRead,
156    #[error("The key owner hasn't been online for more than 7 days")]
157    TemporaryInactivity,
158    #[error("Too many records have been pulled today by this user from our cloud services")]
159    DailyReadLimit,
160    #[error("An error code specifically for testing purposes that has no dedicated meaning")]
161    TemporaryError,
162    #[error("A selection is being called of which this key does not have permission to access")]
163    InsufficientAccessLevel,
164    #[error("Backend error occurred, please try again")]
165    Backend,
166    #[error("API key has been paused by the owner")]
167    Paused,
168    #[error("Must be migrated to crimes 2.0")]
169    NotMigratedCrimes,
170    #[error("Race not yet finished")]
171    RaceNotFinished,
172    #[error("Wrong cat value")]
173    IncorrectCategory,
174    #[error("This selection is only available in API v1")]
175    OnlyInV1,
176    #[error("This selection is only available in API v2")]
177    OnlyInV2,
178    #[error("Closed temporarily")]
179    ClosedTemporarily,
180    #[error("Other: {message}")]
181    Other { code: u16, message: String },
182}
183
184impl ApiError {
185    pub fn new(code: u16, message: &str) -> Self {
186        match code {
187            0 => Self::Unknown,
188            1 => Self::KeyIsEmpty,
189            2 => Self::IncorrectKey,
190            3 => Self::WrongType,
191            4 => Self::WrongFields,
192            5 => Self::TooManyRequest,
193            6 => Self::IncorrectId,
194            7 => Self::IncorrectIdEntityRelation,
195            8 => Self::IpBlock,
196            9 => Self::ApiDisabled,
197            10 => Self::KeyOwnerInFederalJail,
198            11 => Self::KeyChange,
199            12 => Self::KeyRead,
200            13 => Self::TemporaryInactivity,
201            14 => Self::DailyReadLimit,
202            15 => Self::TemporaryError,
203            16 => Self::InsufficientAccessLevel,
204            17 => Self::Backend,
205            18 => Self::Paused,
206            19 => Self::NotMigratedCrimes,
207            20 => Self::RaceNotFinished,
208            21 => Self::IncorrectCategory,
209            22 => Self::OnlyInV1,
210            23 => Self::OnlyInV2,
211            24 => Self::ClosedTemporarily,
212            other => Self::Other {
213                code: other,
214                message: message.to_owned(),
215            },
216        }
217    }
218
219    pub fn code(&self) -> u16 {
220        match self {
221            Self::Unknown => 0,
222            Self::KeyIsEmpty => 1,
223            Self::IncorrectKey => 2,
224            Self::WrongType => 3,
225            Self::WrongFields => 4,
226            Self::TooManyRequest => 5,
227            Self::IncorrectId => 6,
228            Self::IncorrectIdEntityRelation => 7,
229            Self::IpBlock => 8,
230            Self::ApiDisabled => 9,
231            Self::KeyOwnerInFederalJail => 10,
232            Self::KeyChange => 11,
233            Self::KeyRead => 12,
234            Self::TemporaryInactivity => 13,
235            Self::DailyReadLimit => 14,
236            Self::TemporaryError => 15,
237            Self::InsufficientAccessLevel => 16,
238            Self::Backend => 17,
239            Self::Paused => 18,
240            Self::NotMigratedCrimes => 19,
241            Self::RaceNotFinished => 20,
242            Self::IncorrectCategory => 21,
243            Self::OnlyInV1 => 22,
244            Self::OnlyInV2 => 23,
245            Self::ClosedTemporarily => 24,
246            Self::Other { code, .. } => *code,
247        }
248    }
249}
250
251/// Error for invalid parameter values
252#[derive(Debug, Error, PartialEq, Eq)]
253pub enum ParameterError {
254    #[error("value `{value}` is out of range for parameter {name}")]
255    OutOfRange { name: &'static str, value: i32 },
256}
257
258/// Error returned by the default Executor
259#[derive(Debug, Error)]
260pub enum Error {
261    #[error("Parameter error: {0}")]
262    Parameter(#[from] ParameterError),
263    #[cfg(feature = "reqwest")]
264    #[error("Network error: {0}")]
265    Network(#[from] reqwest::Error),
266    #[error("Parsing error: {0}")]
267    Parsing(#[from] serde_json::Error),
268    #[error("Api error: {0}")]
269    Api(#[from] ApiError),
270}