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}