diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a569aaf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,116 @@ +# Tooling +We use the basic Rust tooling. Clippy, rustmft, etc. + +If you have any recommendations regarding a `rustfmt.toml`, please let us know/make a PR. + +# Contributing +If you have a great idea for a feature, let us know [on the Discord](https://discord.gg/jGZxH9f). + +Alternatively, you can open an issue. + +# Project structure +To save you some time, here's a brief explanation of how this project is structured: + +There are 2 modules for the "major" things you might want to do, that is querying the [worldstate](https://docs.warframestat.us) and the [market](https://warframe.market/api_docs) (with the `market` feature). + +The `worldstate` module is much more developed. This is due to the market API getting a V2 soon. + +Since the `market` module is rather small and easy to understand, we'll talk about the `worldstate` module. + +## Worldstate module +All the models are defined via a function-like macro in the `worldstate/models` folder. + +### The `model_builder!` macro +For example, let's look at the definition for `Cetus`: +```rs +model_builder! { + :"The Information about cetus" + Cetus: "/cetusCycle", + rt = obj, + timed = true; + + :"The id of the cycle" + pub id: String, + + :"The state of Cetus (day/night)" + pub state: CetusState, +} +``` +Doc strings are made using the `:"doc here"` syntax. Followed by the `TypeName: "/endpoint_url"`. Said endpoints get concatenated via +```rs +concat!("https://api.warframestat.us/pc", $endpoint, "/?language=en") +``` +at compile time. This prevents unnecessary allocations. Of course, this doesn't work when you want to query in another language. + +When a type has this optional `: "/endpoint"`, it will implement the `Endpoint` trait like so: + +```rs +impl Endpoint for $struct_name { + fn endpoint_en() -> &'static str { + concat!("https://api.warframestat.us/pc", $endpoint, "/?language=en") + } + + #[cfg(feature = "multilangual")] + fn endpoint(language: Language) -> String { + format!( + "https://api.warframestat.us/pc{}/?language={}", + $endpoint, + String::from(language) + ) + } +} +``` + +This is followed by an `rt = obj/arr`, which tells the model in which format it is returned in. +For example, there are no more than 1 `Cetus` at a time, so the API responds with a single `Cetus` object, hence `rt = obj`. `Fissure`s on the other hand have multiple active at a time, so the API responds with an array of those fissures, hence on fissures it's `rt = arr`. + +Next is `timed = true`. This is some trickery, because models who have this set to true will get 2 fields: `activation` and `expiry`, and will additionally implement the `TimedEvent` trait. + +### Putting it all together +To understand this, lets look at the `Queryable` trait first: +```rs +pub trait Queryable: Endpoint { + type Return: DeserializeOwned; + fn query( + request_executor: &reqwest::Client, + ) -> impl std::future::Future> + Send { + async { + Ok(request_executor + .get(Self::endpoint_en()) + .send() + .await? + .json::() + .await?) + } + } + + #[cfg(feature = "multilangual")] + fn query_with_language( + ... +} +``` + +if a model has the endpoint signature (`: "/endpoint"`), the `Queryable` trait will be implemented by the macro. +Based on the `rt`, the `type Return` will either be `Self`, or `Vec`. + +Now, all the `Client`'s `fetch` does: +```rs +impl Client { + pub async fn fetch(&self) -> Result + where + T: Queryable, + { + ::query(&self.session).await + } +} +``` + +This means, depending on the type queried, you get a `Vec`, or a single `Model`. + +E.g. +```rs +let fissures: Vec = client.fetch().await?; +let cetus: Cetus = client.fetch().await?; +``` + +If you have any questions, feel free to ask on the discord, or open an issue. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 7fe731c..db86155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT" [features] default = ["worldstate"] +full = ["worldstate_full", "market_full"] worldstate = [] multilangual = ["worldstate"] worldstate_listeners = ["worldstate"] @@ -22,14 +23,14 @@ market_cache = ["market", "dep:moka"] market_full = ["market", "market_cache"] [dependencies] -tokio = { version = "1.34.0", features = ["full"] } -reqwest = { version = "0.12.5", features = ["json"] } -chrono = { version = "0.4.31", features = ["serde", "clock"] } -serde = { version = "1.0.190", features = ["derive"] } -serde_json = { version = "1.0.108" } -serde_repr = "0.1.18" +tokio = { version = "1.39.3", features = ["full"] } +reqwest = { version = "0.12.7", features = ["json"] } +chrono = { version = "0.4.38", features = ["serde", "clock"] } +serde = { version = "1.0.209", features = ["derive"] } +serde_json = { version = "1.0.127" } +serde_repr = "0.1.19" futures = "0.3.30" -log = "0.4.20" -env_logger = "0.11.1" -thiserror = "1.0.61" -moka = { version = "0.12.7", optional = true, features = ["future"] } +log = "0.4.22" +env_logger = "0.11.5" +thiserror = "1.0.63" +moka = { version = "0.12.8", optional = true, features = ["future"] } diff --git a/README.md b/README.md index 61839ac..2b5e78b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Use this crate if you want to make a Warframe-related rust project that is async To install, simply run `cargo add warframe`. ### Example -```rs +```rust,no_run use warframe::worldstate::prelude::*; #[tokio::main] @@ -31,7 +31,7 @@ async fn main() -> Result<(), ApiError> { ``` ## Contributing -Contributions are more than welcome. To contribute simply fork this repository and make a PR. +See [CONTRIBUTING](CONTRIBUTING.md) ### Commitlint diff --git a/src/lib.rs b/src/lib.rs index 9777635..149f79f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #![doc = include_str!("../README.md")] #[cfg(feature = "worldstate")] +#[forbid(missing_docs)] pub mod worldstate; #[cfg(feature = "market")] diff --git a/src/market/client.rs b/src/market/client.rs index b37068b..fe9363f 100644 --- a/src/market/client.rs +++ b/src/market/client.rs @@ -1,3 +1,6 @@ +//! Provides a client that acts as the baseline for interacting with the market API + +#[allow(unused_imports)] use std::{sync::Arc, time::Duration}; use super::{ @@ -12,13 +15,19 @@ use super::{ #[cfg(feature = "market_cache")] #[derive(Debug, Clone, PartialEq, PartialOrd)] +#[doc = "A cached value"] pub enum CacheValue { + /// StatisticItem StatisticItem(Arc), + /// ItemInfo ItemInfo(Arc), + /// Items Items(Arc>), + /// Orders Orders(Arc>), } +/// The client #[derive(Debug, Clone)] #[cfg_attr(not(feature = "market_cache"), derive(Default))] pub struct Client { @@ -28,6 +37,7 @@ pub struct Client { } impl Client { + /// Creates a new [Client] pub fn new() -> Self { Default::default() } @@ -62,7 +72,9 @@ impl Client { .await?; if response.status().is_success() { - let json_result = response.json::().await?; + let json_result = response + .json::() + .await?; Ok(json_result.payload.item) } else { Err(response.status().into()) @@ -78,7 +90,9 @@ impl Client { .await?; if response.status().is_success() { - let json_result = response.json::().await?; + let json_result = response + .json::() + .await?; Ok(json_result.payload.items) } else { Err(response.status().into()) @@ -96,7 +110,9 @@ impl Client { .await?; if response.status().is_success() { - let json_result = response.json::().await?; + let json_result = response + .json::() + .await?; Ok(json_result.payload.orders) } else { Err(response.status().into()) @@ -104,6 +120,7 @@ impl Client { } } +/// The cached version of the client #[cfg(feature = "market_cache")] pub mod cached { use { @@ -117,13 +134,17 @@ pub mod cached { pub use moka; use reqwest::Response; + /// Whether an item has been gotten via a cache hit or freshly fetched. pub enum FetchResult { + /// Cache hit Cached(CacheValue), + /// Fetched Fetched(Result), } #[cfg(feature = "market_cache")] impl Client { + /// Creates a new client with a custom cache pub fn new_with_cache(cache: Cache) -> Self { Self { session: Default::default(), diff --git a/src/market/error.rs b/src/market/error.rs index 4b5b12c..d99f362 100644 --- a/src/market/error.rs +++ b/src/market/error.rs @@ -1,11 +1,17 @@ +//! This module defines error types + +/// The market's error type #[derive(Debug, thiserror::Error)] pub enum ApiError { + /// An error from the sent request #[error("Couldn't send request: {0}")] FaultyRequest(#[from] reqwest::Error), + /// An error that occurs when the deserialization of serde_json fails #[error("Couldn't deserialize json body: {0}")] FailedDeserialization(#[from] serde_json::Error), + /// Any error directly from the API (status code only) #[error("Error response from the API: {0}")] ApiError(reqwest::StatusCode), } diff --git a/src/market/mod.rs b/src/market/mod.rs index 9e55a8a..5f19055 100644 --- a/src/market/mod.rs +++ b/src/market/mod.rs @@ -1,3 +1,5 @@ +//! Implementation for the market module, used to interact with the warframe.market API + pub mod client; pub mod error; pub mod models; diff --git a/src/market/models/item.rs b/src/market/models/item.rs index 7ca56f9..dda4e20 100644 --- a/src/market/models/item.rs +++ b/src/market/models/item.rs @@ -12,15 +12,20 @@ pub(crate) struct Items { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd)] pub struct Item { + /// thumb pub thumb: String, + /// item_name pub item_name: String, + /// url_name pub url_name: String, + /// id pub id: String, #[serde(default)] + /// vaulted pub vaulted: bool, } diff --git a/src/market/models/item_info.rs b/src/market/models/item_info.rs index 22bb625..9c70b85 100644 --- a/src/market/models/item_info.rs +++ b/src/market/models/item_info.rs @@ -47,16 +47,22 @@ pub struct ItemInSet { pub info: LanguageItem, } +/// A Language Item #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd)] pub struct LanguageItem { + /// item_name pub item_name: String, + /// description pub description: String, + /// wiki_link pub wiki_link: String, + /// thumb pub thumb: String, + /// icon pub icon: String, // drop: Vec>, // seems to be empty all the time } diff --git a/src/market/models/mod.rs b/src/market/models/mod.rs index 6dc802d..16fb348 100644 --- a/src/market/models/mod.rs +++ b/src/market/models/mod.rs @@ -1,4 +1,9 @@ -pub mod item; -pub mod item_info; -pub mod orders; -pub mod statistic_item; +pub(crate) mod item; +pub(crate) mod item_info; +pub(crate) mod orders; +pub(crate) mod statistic_item; + +pub use item::*; +pub use item_info::*; +pub use orders::*; +pub use statistic_item::*; diff --git a/src/market/models/statistic_item.rs b/src/market/models/statistic_item.rs index 602cf7c..30d936e 100644 --- a/src/market/models/statistic_item.rs +++ b/src/market/models/statistic_item.rs @@ -1,4 +1,5 @@ use { + super::OrderType, chrono::{DateTime, Utc}, serde::{Deserialize, Serialize}, }; @@ -84,13 +85,6 @@ pub struct StatisticsLive48Hour { pub id: String, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd)] -#[serde(rename_all = "snake_case")] -pub enum OrderType { - Buy, - Sell, -} - #[cfg(test)] mod test { use crate::market::{client::Client, error::ApiError}; diff --git a/src/worldstate/client.rs b/src/worldstate/client.rs index b090159..2b735f5 100644 --- a/src/worldstate/client.rs +++ b/src/worldstate/client.rs @@ -1,15 +1,35 @@ +//! A client to do all sorts of things with the API + use crate::ws::Queryable; #[allow(unused)] use crate::ws::TimedEvent; use super::error::ApiError; +/// The client that acts as a convenient way to query models. +/// +/// ## Example +/// ```rust,no_run +/// use warframe::worldstate::prelude as wf; +/// #[tokio::main] +/// async fn main() -> Result<(), wf::ApiError> { +/// let client = wf::Client::new(); +/// +/// let cetus: wf::Cetus = client.fetch::().await?; +/// let fissures: Vec = client.fetch::().await?; +/// +/// Ok(()) +/// } +/// ``` +/// +/// Check [Models](crate::worldstate::models) for an alternative way of querying/[`fetch`](Client::fetch)ing. #[derive(Default, Debug, Clone)] pub struct Client { session: reqwest::Client, } impl Client { + /// Creates a new [Client]. pub fn new() -> Self { Default::default() } @@ -17,12 +37,55 @@ impl Client { // impl FETCH impl Client { + /// Fetches an instance of a specified model. + /// + /// # Examples + /// + /// ```rust + /// use warframe::worldstate::prelude as wf; + /// #[tokio::main] + /// async fn main() -> Result<(), wf::ApiError> { + /// let client = wf::Client::new(); + /// + /// let cetus: wf::Cetus = client.fetch::().await?; + /// let fissures: Vec = client.fetch::().await?; + /// + /// Ok(()) + /// } + /// ``` pub async fn fetch(&self) -> Result where T: Queryable, { ::query(&self.session).await } + + /// Fetches an instance of a specified model in a supplied Language. + /// + /// # Examples + /// + /// ```rust + /// use warframe::worldstate::prelude as wf; + /// #[tokio::main] + /// async fn main() -> Result<(), wf::ApiError> { + /// let client = wf::Client::new(); + /// + /// let cetus: wf::Cetus = client.fetch_using_lang::(wf::Language::ZH).await?; + /// let fissures: Vec = client.fetch_using_lang::(wf::Language::ZH).await?; + /// + /// Ok(()) + /// } + /// ``` + #[cfg(feature = "multilangual")] + pub async fn fetch_using_lang( + &self, + language: crate::ws::Language, + ) -> Result + where + T: Queryable, + { + T::query_with_language(&self.session, language).await + } } // impl UPDATE LISTENER @@ -46,26 +109,26 @@ impl Client { /// # Example /// /// ```rust - ///use std::error::Error; + /// use std::error::Error; /// - ///use warframe::worldstate::prelude::*; + /// use warframe::worldstate::prelude::*; /// - ///async fn on_cetus_update(before: &Cetus, after: &Cetus) { - /// println!("BEFORE : {before:?}"); - /// println!("AFTER : {after:?}"); - ///} + /// async fn on_cetus_update(before: &Cetus, after: &Cetus) { + /// println!("BEFORE : {before:?}"); + /// println!("AFTER : {after:?}"); + /// } /// - ///#[tokio::main] - ///async fn main() -> Result<(), Box> { - /// env_logger::builder() - /// .filter_level(log::LevelFilter::Debug) - /// .init(); + /// #[tokio::main] + /// async fn main() -> Result<(), Box> { + /// env_logger::builder() + /// .filter_level(log::LevelFilter::Debug) + /// .init(); /// - /// let client = Client::new(); - /// - /// client.call_on_update(on_cetus_update); // don't forget to start it as a bg task (or .await it)s - /// Ok(()) - ///} + /// let client = Client::new(); + /// + /// client.call_on_update(on_cetus_update); // don't forget to start it as a bg task (or .await it)s + /// Ok(()) + /// } /// /// ``` pub async fn call_on_update(&self, callback: Callback) -> Result<(), ApiError> @@ -151,7 +214,6 @@ impl Client { /// client.call_on_nested_update(on_fissure_update); // don't forget to start it as a bg task (or .await it) /// Ok(()) /// } - /// /// ``` pub async fn call_on_nested_update( &self, @@ -396,17 +458,3 @@ impl Client { } } } - -// impl FETCH (with language) -#[cfg(feature = "multilangual")] -impl Client { - pub async fn fetch_using_lang( - &self, - language: crate::ws::Language, - ) -> Result - where - T: Queryable, - { - T::query_with_language(&self.session, language).await - } -} diff --git a/src/worldstate/error.rs b/src/worldstate/error.rs index f1aa412..fa81551 100644 --- a/src/worldstate/error.rs +++ b/src/worldstate/error.rs @@ -1,24 +1,29 @@ +//! Definitions for errors use serde::Deserialize; /// The `OriginalError` struct represents an error with a string message and an error code. /// This is "as is", meaning this is how the API returns this error. #[derive(Debug, Deserialize, thiserror::Error)] -#[error("")] +#[error("{error} [CODE {code}]")] pub struct ApiErrorResponse { /// The error message pub error: String, - // The status code returned + /// The status code returned pub code: u16, } +/// The Error type of this crate #[derive(Debug, thiserror::Error)] pub enum ApiError { + /// An error from the sent request #[error("Couldn't send request: {0}")] FaultyRequest(#[from] reqwest::Error), + /// An error that occurs when the deserialization of serde_json fails #[error("Couldn't deserialize json body: {0}")] FailedDeserialization(#[from] serde_json::Error), + /// Any error directly from the API #[error("Error response from the API: {0}")] ApiError(#[from] ApiErrorResponse), } diff --git a/src/worldstate/language.rs b/src/worldstate/language.rs index ecf4496..cd9e277 100644 --- a/src/worldstate/language.rs +++ b/src/worldstate/language.rs @@ -1,14 +1,28 @@ +//! Adds an enum that represents the different languages that are supported by the API + +/// An enumeration representing various supported languages. pub enum Language { + /// German (`DE`) DE, + /// Spanish (`ES`) ES, + /// French (`FR`) FR, + /// Italian (`IT`) IT, + /// Korean (`KO`) KO, + /// Polish (`PL`) PL, + /// Portuguese (`PT`) PT, + /// Russian (`RU`) RU, + /// Chinese (`ZH`) ZH, + /// English (`EN`) EN, + /// Ukrainian (`UK`) UK, } diff --git a/src/worldstate/listener.rs b/src/worldstate/listener.rs index 5ca534b..22f8acc 100644 --- a/src/worldstate/listener.rs +++ b/src/worldstate/listener.rs @@ -1,17 +1,87 @@ +//! # Allows registering listeners. +//! +//! Listeners are functions that will be called when a specific endpoint receives an update. +//! +//! There are 2 types. The 'normal' ones for models like [Cetus](crate::worldstate::models::Cetus), +//! and 'nested' ones for models like [Fissure](crate::worldstate::models::Fissure). +//! +//! ### Demo for the 'normal' listeners +//! ```rust,no_run +//! use std::error::Error; +//! +//! use warframe::worldstate::prelude::*; +//! +//! async fn on_cetus_update(before: &Cetus, after: &Cetus) { +//! println!("BEFORE : {before:?}"); +//! println!("AFTER : {after:?}"); +//! } +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! env_logger::builder() +//! .filter_level(log::LevelFilter::Debug) +//! .init(); +//! +//! let client = Client::new(); +//! +//! client.call_on_update(on_cetus_update); // don't forget to start it as a bg task (or .await it) +//! Ok(()) +//! } +//! ``` +//! +//! ### Demo for the 'nested' listeners +//! ```rust +//! use std::error::Error; +//! +//! use warframe::worldstate::{listener::Change, prelude::*}; +//! +//! /// This function will be called once a fissure updates. +//! /// This will send a request to the corresponding endpoint once every 30s +//! /// and compare the results for changes. +//! async fn on_fissure_update(fissure: &Fissure, change: Change) { +//! match change { +//! Change::Added => println!("Fissure ADDED : {fissure:?}"), +//! Change::Removed => println!("Fissure REMOVED : {fissure:?}"), +//! } +//! } +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Logging setup +//! env_logger::builder() +//! .filter_level(log::LevelFilter::Debug) +//! .init(); +//! +//! // initialize a client (included in the prelude) +//! let client = Client::new(); +//! +//! // Pass the function to the handler +//! // (will return a Future) +//! client.call_on_nested_update(on_fissure_update); // don't forget to start it as a bg task (or .await it) +//! Ok(()) +//! } +//! ``` + use std::future::Future; +/// Represents what has happened to the nested Item. #[derive(PartialEq, PartialOrd, Debug, Clone)] pub enum Change { + /// The Item has been added to the collection Added, + /// The Item has been removed the collection Removed, } // ---------- +/// A listener callback that can listen to any model with [`Queryable::Return`](crate::worldstate::models::base::Queryable::Return) = Self pub trait ListenerCallback<'a, T> where T: Sized + 'a, { + /// Type of the future type Fut: Future + Send; + /// The method to call the handler fn call(&self, before: &'a T, after: &'a T) -> Self::Fut; } @@ -27,11 +97,14 @@ where } } +/// A listener callback that can listen to any model with [`Queryable::Return`](crate::worldstate::models::base::Queryable::Return) = Vec pub trait NestedListenerCallback<'a, T> where T: Sized, { + /// Type of the future type Fut: Future + Send; + /// The method to call the handler fn call(&self, item: &'a T, change: Change) -> Self::Fut; } @@ -48,12 +121,17 @@ where } // --------- STATEFUL CALLBACKS +/// A listener callback that can listen to any model with [`Queryable::Return`](crate::worldstate::models::base::Queryable::Return) = Self +/// +/// and a state. pub trait StatefulListenerCallback<'a, T, S> where T: Sized, S: Sized + Send + Sync, { + /// Type of the future type Fut: Future + Send; + /// The method to call the handler fn call_with_state(&self, state: S, before: &'a T, after: &'a T) -> Self::Fut; } @@ -70,12 +148,17 @@ where } } +/// A listener callback that can listen to any model with [`Queryable::Return`](crate::worldstate::models::base::Queryable::Return) = Vec +/// +/// and a state. pub trait StatefulNestedListenerCallback<'a, T, S> where T: Sized, S: Sized + Send + Sync, { + /// Type of the future type Fut: Future + Send; + /// The method to call the handler fn call_with_state(&self, state: S, item: &'a T, change: Change) -> Self::Fut; } @@ -92,6 +175,7 @@ where } } +/// A type that implements logic to find which items have been added or removed in 2 collections pub struct CrossDiff<'a, T> where T: PartialEq, @@ -104,10 +188,12 @@ impl<'a, T> CrossDiff<'a, T> where T: PartialEq, { + /// Creates a [CrossDiff] pub fn new(current: &'a [T], incoming: &'a [T]) -> Self { Self { current, incoming } } + /// Gets all the removed items pub fn removed(&self) -> Vec<(&'a T, Change)> { self.current .iter() @@ -116,6 +202,7 @@ where .collect() } + /// Gets all the added items pub fn added(&self) -> Vec<(&'a T, Change)> { self.incoming .iter() diff --git a/src/worldstate/mod.rs b/src/worldstate/mod.rs index 5ae0a37..ae5af32 100644 --- a/src/worldstate/mod.rs +++ b/src/worldstate/mod.rs @@ -1,3 +1,22 @@ +//! # The worldstate module +//! +//! Get information about various different parts of the game. +//! Check [] +//! +//! ## Quickstart +//! ```rust,no_run +//! use warframe::worldstate::prelude as wf; +//! #[tokio::main] +//! async fn main() -> Result<(), wf::ApiError> { +//! let client = wf::Client::new(); +//! +//! let cetus: wf::Cetus = client.fetch::().await?; +//! let fissures: Vec = client.fetch::().await?; +//! +//! Ok(()) +//! } +//! ``` + pub mod client; pub mod error; pub mod models; @@ -8,6 +27,7 @@ pub mod language; #[cfg(feature = "worldstate_listeners")] pub mod listener; +/// The prelude which contains most things you need. pub mod prelude { pub use crate::worldstate::client::Client; pub use crate::worldstate::error::{ApiError, ApiErrorResponse}; diff --git a/src/worldstate/models/arbitration.rs b/src/worldstate/models/arbitration.rs index 4717830..3359a89 100644 --- a/src/worldstate/models/arbitration.rs +++ b/src/worldstate/models/arbitration.rs @@ -87,6 +87,7 @@ pub struct Arbitration { } impl Arbitration { + /// Whether the arbitration is still valid. pub fn is_valid(&self) -> bool { self.expiry() != DateTime::::MAX_UTC } diff --git a/src/worldstate/models/archon_hunt.rs b/src/worldstate/models/archon_hunt.rs index 2d759ef..bd12b23 100644 --- a/src/worldstate/models/archon_hunt.rs +++ b/src/worldstate/models/archon_hunt.rs @@ -30,6 +30,7 @@ model_builder! { :"Whether the mission is a sharkwing mission" pub is_sharkwing: bool, + :"Any additional spawners" pub advanced_spawners: Vec, :"Items required to enter the mission" diff --git a/src/worldstate/models/base.rs b/src/worldstate/models/base.rs index 44d3e03..ccef289 100644 --- a/src/worldstate/models/base.rs +++ b/src/worldstate/models/base.rs @@ -1,11 +1,20 @@ +//! Here lies what powers the models. + use chrono::{DateTime, Utc}; use serde::{de::DeserializeOwned, Deserialize}; use std::ops::{Div, Rem}; +/// Types implementing this have an actual endpoint on the API. pub trait Endpoint { + /// Returns the URL to the english endpoint. + /// + /// Getting the english endpoint is free, as concatenating is done at compile time. fn endpoint_en() -> &'static str; + /// Returns the URL to the endpoint of the specified language. + /// + /// Getting this endpoint is __NOT__ free, as concatenating is done at runtime. #[cfg(feature = "multilangual")] fn endpoint(language: crate::worldstate::language::Language) -> String; } @@ -48,8 +57,14 @@ pub trait TimedEvent { } } +/// Marks a struct as `Queryable`. +/// +/// Comes with a default implementation that works universally. pub trait Queryable: Endpoint { + /// The Type returned by the [query](Queryable::query). type Return: DeserializeOwned; + + /// Queries a model and returns an instance of ['itself'](Queryable::Return). fn query( request_executor: &reqwest::Client, ) -> impl std::future::Future> + Send { @@ -63,6 +78,7 @@ pub trait Queryable: Endpoint { } } + /// Queries a model with the specified language and returns an instance of ['itself'](Queryable::Return). #[cfg(feature = "multilangual")] fn query_with_language( request_executor: &reqwest::Client, @@ -124,24 +140,24 @@ pub(crate) fn get_short_format_time_string(dt: DateTime) -> String { formatted_time.trim().to_string() } +/// A trait allowing to get the documentation of an enum variant - so you don't have to write it. pub trait VariantDocumentation { /// Gets the documentation for this variant fn docs(&self) -> &'static str; } +/// A trait allowing to get the documentation of a type - so you don't have to write it. pub trait TypeDocumentation { /// Gets the documentation for this Enum fn docs() -> &'static str; } +/// A trait that allows enums with 2 variants to easily access the other variant. pub trait Opposite { - #[doc = "Returns the opposite of this state"] + /// Returns the opposite of this state fn opposite(&self) -> Self; } -#[cfg(feature = "macros")] -pub use macro_features::*; - use crate::worldstate::error::ApiError; #[cfg(test)] diff --git a/src/worldstate/models/cambion_drift.rs b/src/worldstate/models/cambion_drift.rs index d3f94b6..7b28f53 100644 --- a/src/worldstate/models/cambion_drift.rs +++ b/src/worldstate/models/cambion_drift.rs @@ -4,12 +4,14 @@ enum_builder! { :"The State of the Cambion Drift" CambionDriftState; + :"The 'Vome' state" Vome = "vome", + :"The 'Fass' state" Fass = "fass" } model_builder! { - :"Struct representing Cambion Drift info" + :"Cambion Drift info" CambionDrift: "/cambionCycle", rt = obj, timed = true; diff --git a/src/worldstate/models/construction_progress.rs b/src/worldstate/models/construction_progress.rs index 3705ae3..131012f 100644 --- a/src/worldstate/models/construction_progress.rs +++ b/src/worldstate/models/construction_progress.rs @@ -8,7 +8,10 @@ model_builder! { rt = obj, timed = false; + :"The progress of the Fomorian" pub fomorian_progress: f32 => "deserialize_f32_from_string", + + :"The progress of the Razorback" pub razorback_progress: f32 => "deserialize_f32_from_string", :"No clue what this is tbh" diff --git a/src/worldstate/models/faction.rs b/src/worldstate/models/faction.rs index 4001ea7..84487da 100644 --- a/src/worldstate/models/faction.rs +++ b/src/worldstate/models/faction.rs @@ -2,15 +2,26 @@ use super::macros::enum_builder; enum_builder! { :"A Faction in Warframe" Faction; + :"Orokin" Orokin, + :"Corrupted" Corrupted, + :"Infested" Infested, + :"Infestation" Infestation, + :"Corpus" Corpus, + :"Grineer" Grineer, + :"Tenno" Tenno, + :"Narmer" Narmer, + :"Crossfire" Crossfire, + :"Murmur" Murmur = "The Murmur", + :"ManInTheWall" ManInTheWall = "Man in the Wall" } diff --git a/src/worldstate/models/fissure.rs b/src/worldstate/models/fissure.rs index f115f79..9767057 100644 --- a/src/worldstate/models/fissure.rs +++ b/src/worldstate/models/fissure.rs @@ -3,16 +3,24 @@ use super::macros::{enum_builder, model_builder}; use super::mission_type::MissionType; enum_builder! { + :"Represents Relic tiers" Tier; + :"Lith" Lith: 1, + :"Meso" Meso: 2, + :"Neo" Neo: 3, + :"Axi" Axi: 4, + :"Requiem" Requiem: 5, + :"Omnia" Omnia: 6, } model_builder! { + :"A Fissure Mission in which you can crack Void Relics" Fissure: "/fissures", rt = array, timed = true; diff --git a/src/worldstate/models/flash_sale.rs b/src/worldstate/models/flash_sale.rs index ab6e653..421ff73 100644 --- a/src/worldstate/models/flash_sale.rs +++ b/src/worldstate/models/flash_sale.rs @@ -12,8 +12,12 @@ model_builder! { :"The discount of the Item" pub discount: i32, + :"The PLATINUM price of this item" pub premium_override: i32, + :"The CREDIT price of this item" + pub regular_override: i32, + :"Whether the item is popular or not" pub is_popular: Option, diff --git a/src/worldstate/models/invasion.rs b/src/worldstate/models/invasion.rs index c330283..2e113ef 100644 --- a/src/worldstate/models/invasion.rs +++ b/src/worldstate/models/invasion.rs @@ -4,6 +4,7 @@ use super::{Faction, Reward, RewardType}; type DateTime = chrono::DateTime; model_builder! { + :"An defender/attacker of an Invasion" InvasionMember, rt = obj, timed = false; @@ -19,6 +20,7 @@ model_builder! { } model_builder! { + :"An Invasion" Invasion: "/invasions", rt = array, timed = false; diff --git a/src/worldstate/models/macros.rs b/src/worldstate/models/macros.rs index 0eba9d7..57ee3ce 100644 --- a/src/worldstate/models/macros.rs +++ b/src/worldstate/models/macros.rs @@ -24,32 +24,32 @@ macro_rules! model_builder { // --------------------------------- macro_rules! impl_model_struct { ( - @timed = false $(:$struct_doc:literal)? $struct_name:ident; - $($(:$field_doc:literal)? $visibility:vis $field:ident : $field_type:ty $(= $rename:literal)? $(=> $deserialize_func:literal)?),*) => { + @timed = false :$struct_doc:literal $struct_name:ident; + $(:$field_doc:literal $visibility:vis $field:ident : $field_type:ty $(= $rename:literal)? $(=> $deserialize_func:literal)?),*) => { #[derive(Debug, serde::Deserialize, PartialEq, PartialOrd, Clone)] #[serde(rename_all = "camelCase")] - $(#[doc = $struct_doc])? + #[doc = $struct_doc] pub struct $struct_name { $( $(#[serde(rename(deserialize = $rename))])? $(#[serde(deserialize_with = $deserialize_func)])? - $(#[doc = $field_doc])? + #[doc = $field_doc] $visibility $field : $field_type, )* } }; ( - @timed = true $(:$struct_doc:literal)? $struct_name:ident; - $($(:$field_doc:literal)? $visibility:vis $field:ident : $field_type:ty $(= $rename:literal)? $(=> $deserialize_func:literal)?),*) => { + @timed = true :$struct_doc:literal $struct_name:ident; + $(:$field_doc:literal $visibility:vis $field:ident : $field_type:ty $(= $rename:literal)? $(=> $deserialize_func:literal)?),*) => { #[derive(Debug, serde::Deserialize, PartialEq, PartialOrd, Clone)] #[serde(rename_all = "camelCase")] - $(#[doc = $struct_doc])? + #[doc = $struct_doc] pub struct $struct_name { $( $(#[serde(rename(deserialize = $rename))])? $(#[serde(deserialize_with = stringify!($deserialize_func))])? - $(#[doc = $field_doc])? + #[doc = $field_doc] $visibility $field : $field_type, )* diff --git a/src/worldstate/models/mission.rs b/src/worldstate/models/mission.rs index e700e48..2a501fd 100644 --- a/src/worldstate/models/mission.rs +++ b/src/worldstate/models/mission.rs @@ -27,6 +27,7 @@ model_builder! { :"The maximum level of the enemy" pub max_enemy_level: i32, + :"The maximum wave you can get to" pub max_wave_num: Option, :"The i18n type of the mission" @@ -44,10 +45,13 @@ model_builder! { :"Whether the mission is a sharkwing mission" pub is_sharkwing: bool, + :"The enemy spec" pub enemy_spec: String, + :"Any level override" pub level_override: String, + :"Any additional spawners" pub advanced_spawners: Vec, :"Items required to enter the mission" diff --git a/src/worldstate/models/mission_type.rs b/src/worldstate/models/mission_type.rs index bb6570b..741be22 100644 --- a/src/worldstate/models/mission_type.rs +++ b/src/worldstate/models/mission_type.rs @@ -2,46 +2,88 @@ use super::macros::enum_builder; enum_builder! { :"A Mission Type in Warframe" MissionType; + :"AncientRetribution" AncientRetribution = "Ancient Retribution", + :"Arena" Arena, + :"Assassination" Assassination, + :"Assault" Assault, + :"Capture" Capture, + :"Conclave" Conclave, + :"DarkSectorDefection" DarkSectorDefection = "Dark Sector Defection", + :"DarkSectorDefense" DarkSectorDefense = "Dark Sector Defense", + :"DarkSectorDisruption" DarkSectorDisruption = "Dark Sector Disruption", + :"DarkSectorExcavation" DarkSectorExcavation = "Dark Sector Excavation", + :"DarkSectorSabotage" DarkSectorSabotage = "Dark Sector Sabotage", + :"DarkSectorSurvival" DarkSectorSurvival = "Dark Sector Survival", + :"Defense" Defense, + :"Disruption" Disruption, + :"Excavation" Excavation, + :"ExterminationArchwing" ExterminationArchwing = "Extermination (Archwing)", + :"Extermination" Extermination, + :"FreeRoam" FreeRoam = "Free Roam", + :"Hijack" Hijack, + :"Hive" Hive, + :"HiveSabotage" HiveSabotage = "Hive Sabotage", + :"Interception" Interception, + :"InterceptionArchwing" InterceptionArchwing = "Interception (Archwing)", + :"MobileDefense" MobileDefense = "Mobile Defense", + :"MobileDefenseArchwing" MobileDefenseArchwing = "Mobile Defense (Archwing)", + :"OrokinSabotage" OrokinSabotage = "Orokin Sabotage", + :"Orphix" Orphix, + :"PursuitArchwing" PursuitArchwing = "Pursuit (Archwing)", + :"Relay" Relay, + :"Rescue" Rescue, + :"RushArchwing" RushArchwing = "Rush (Archwing)", + :"Sabotage" Sabotage, + :"SabotageArchwing" SabotageArchwing = "Sabotage (Archwing)", + :"Skirmish" Skirmish, + :"Spy" Spy, + :"Survival" Survival, + :"Volatile" Volatile, + :"Alchemy" Alchemy, + :"Corruption" Corruption, + :"VoidCascade" VoidCascade = "Void Cascade", + :"Defection" Defection, + :"Unknown" Unknown, } diff --git a/src/worldstate/models/mod.rs b/src/worldstate/models/mod.rs index 56db8b1..b9926ea 100644 --- a/src/worldstate/models/mod.rs +++ b/src/worldstate/models/mod.rs @@ -1,7 +1,32 @@ +//! # Models +//! All Models can be found here. +//! Some of them are queryable. +//! +//! You can query every model that implements [Queryable](crate::worldstate::models::base::Queryable) [CLient](crate::worldstate::client::Client). +//! # Querying... +//! +//! ### ...through the client +//! To query models through the provided client, see [Client](crate::worldstate::client::Client) +//! +//! ### ...via the [Queryable] trait +//! ```rust +//! use warframe::worldstate::prelude as wf; +//! use warframe::worldstate::prelude::Queryable; +//! #[tokio::main] +//! async fn main() -> Result<(), wf::ApiError> { +//! let reqwest_client = reqwest::Client::new(); +//! +//! let cetus: wf::Cetus = wf::Cetus::query(&reqwest_client).await?; +//! let fissures: Vec = wf::Fissure::query(&reqwest_client).await?; +//! +//! Ok(()) +//! } +//! ``` + mod alert; mod arbitration; mod archon_hunt; -pub(crate) mod base; +pub mod base; mod cambion_drift; mod cetus; mod construction_progress; @@ -26,10 +51,7 @@ mod syndicate; mod syndicate_mission; mod void_trader; -pub use base::{Endpoint, Opposite, TimedEvent, TypeDocumentation, VariantDocumentation}; - -#[cfg(feature = "macros")] -pub use base::Change; +pub use base::*; pub use alert::Alert; pub use arbitration::Arbitration; @@ -55,3 +77,16 @@ pub use steel_path::{SteelPath, SteelPathShopItem}; pub use syndicate::Syndicate; pub use syndicate_mission::{SyndicateJob, SyndicateMission}; pub use void_trader::{VoidTrader, VoidTraderInventoryItem}; + +#[tokio::test] +async fn test_doc_example() -> Result<(), crate::worldstate::prelude::ApiError> { + use crate::worldstate::prelude as wf; + use crate::worldstate::prelude::Queryable; + + let client = reqwest::Client::new(); + + let _cetus: wf::Cetus = Cetus::query(&client).await?; + let _fissures: Vec = Fissure::query(&client).await?; + + Ok(()) +} diff --git a/src/worldstate/models/nightwave.rs b/src/worldstate/models/nightwave.rs index a5c8cc5..5301973 100644 --- a/src/worldstate/models/nightwave.rs +++ b/src/worldstate/models/nightwave.rs @@ -4,10 +4,15 @@ use super::{ }; enum_builder! { + :"Represents the difficulty of a [Nightwave Challenge](NightwaveChallenge)" NightwaveChallengeType; + :"Easy" Easy, + :"Medium" Medium, + :"Hard" Hard, + :"Unknown" Unknown, } @@ -40,6 +45,7 @@ model_builder! { } impl NightwaveChallenge { + /// Gets the difficulty for this challenge pub fn challenge_type(&self) -> NightwaveChallengeType { use NightwaveChallengeType::*; if self.is_permanent { @@ -70,8 +76,10 @@ model_builder! { :"The Tag of this Nightwave" pub tag: String, + :"The phase of the nightwave" pub phase: i32, + :"The reward types" pub reward_types: Vec, :"The active challenges (most likely the weekly rotation)" diff --git a/src/worldstate/models/orb_vallis.rs b/src/worldstate/models/orb_vallis.rs index 8b0f9fb..becae5d 100644 --- a/src/worldstate/models/orb_vallis.rs +++ b/src/worldstate/models/orb_vallis.rs @@ -4,7 +4,9 @@ enum_builder! { :"Represents the state on Orb Vallis" OrbVallisState; + :"Warm" Warm = "warm", + :"Cold" Cold = "cold" } diff --git a/src/worldstate/models/reward.rs b/src/worldstate/models/reward.rs index ba7c303..a5ca36a 100644 --- a/src/worldstate/models/reward.rs +++ b/src/worldstate/models/reward.rs @@ -1,11 +1,15 @@ use super::macros::model_builder; model_builder! { + :"Small info about how many of which items are given" CountedItem, rt = array, timed = false; + :"How many of this item" pub count: i32, + + :"The item type" pub r#type: String } diff --git a/src/worldstate/models/reward_type.rs b/src/worldstate/models/reward_type.rs index 91d3a8d..1ec86fa 100644 --- a/src/worldstate/models/reward_type.rs +++ b/src/worldstate/models/reward_type.rs @@ -1,48 +1,92 @@ use super::macros::enum_builder; enum_builder! { + :"Represents different reward types" RewardType; + :"Vauban" Vauban = "vauban", + :"Vandal" Vandal = "vandal", + :"Wraith" Wraith = "wraith", + :"Skin" Skin = "skin", + :"Helmet" Helmet = "helmet", + :"Nitain" Nitain = "nitain", + :"Mutalist" Mutalist = "mutalist", + :"Weapon" Weapon = "weapon", + :"Fieldron" Fieldron = "fieldron", + :"Detonite" Detonite = "detonite", + :"Mutagen" Mutagen = "mutagen", + :"Aura" Aura = "aura", + :"NeuralSensors" NeuralSensors = "neuralSensors", + :"OrokinCell" OrokinCell = "orokinCell", + :"AlloyPlate" AlloyPlate = "alloyPlate", + :"Circuits" Circuits = "circuits", + :"ControlModule" ControlModule = "controlModule", + :"Ferrite" Ferrite = "ferrite", + :"Gallium" Gallium = "gallium", + :"Morphics" Morphics = "morphics", + :"NanoSpores" NanoSpores = "nanoSpores", + :"Oxium" Oxium = "oxium", + :"Rubedo" Rubedo = "rubedo", + :"Salvage" Salvage = "salvage", + :"Plastids" Plastids = "plastids", + :"PolymerBundle" PolymerBundle = "polymerBundle", + :"ArgonCrystal" ArgonCrystal = "argonCrystal", + :"Cryotic" Cryotic = "cryotic", + :"Tellurium" Tellurium = "tellurium", + :"Neurodes" Neurodes = "neurodes", + :"Nightmare" Nightmare = "nightmare", + :"Endo" Endo = "endo", + :"Reactor" Reactor = "reactor", + :"Catalyst" Catalyst = "catalyst", + :"Forma" Forma = "forma", + :"Synthula" Synthula = "synthula", + :"Exilus" Exilus = "exilus", + :"Riven" Riven = "riven", + :"KavatGene" KavatGene = "kavatGene", + :"KubrowEgg" KubrowEgg = "kubrowEgg", + :"Traces" Traces = "traces", + :"Other" Other = "other", + :"Credits" Credits = "credits", } diff --git a/src/worldstate/models/syndicate.rs b/src/worldstate/models/syndicate.rs index 45c9379..9dca394 100644 --- a/src/worldstate/models/syndicate.rs +++ b/src/worldstate/models/syndicate.rs @@ -3,21 +3,38 @@ use super::macros::enum_builder; enum_builder! { :"A Syndicate in Warframe" Syndicate; + :"ArbitersOfHexis" ArbitersOfHexis = "Arbiters of Hexis", + :"CephalonSuda" CephalonSuda = "Cephalon Suda", + :"Assassins" Assassins = "Assassins", + :"Nightwave" Nightwave = "Nightwave", + :"Ostrons" Ostrons = "Ostrons", + :"VoxSolaris" VoxSolaris = "Vox Solaris", + :"SolarisUnited" SolarisUnited = "Solaris United", + :"PerrinSequence" PerrinSequence = "Perrin Sequence", + :"SteelMeridian" SteelMeridian = "Steel Meridian", + :"RedVeil" RedVeil = "Red Veil", + :"NewLoka" NewLoka = "New Loka", + :"Holdfasts" Holdfasts = "The Holdfasts", + :"Entrati" Entrati, + :"Cavia" Cavia = "EntratiLabSyndicate", + :"VentKids" VentKids = "Operations Syndicate", + :"KahlsGarrison" KahlsGarrison = "Kahl's Garrison", + :"Necraloid" Necraloid, }