Skip to content

Commit

Permalink
docs: better docs (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mettwasser authored Aug 26, 2024
1 parent e06e1d9 commit 03c54d1
Show file tree
Hide file tree
Showing 34 changed files with 612 additions and 77 deletions.
116 changes: 116 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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<Output = Result<Self::Return, ApiError>> + Send {
async {
Ok(request_executor
.get(Self::endpoint_en())
.send()
.await?
.json::<Self::Return>()
.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<Self>`.

Now, all the `Client`'s `fetch` does:
```rs
impl Client {
pub async fn fetch<T>(&self) -> Result<T::Return, ApiError>
where
T: Queryable,
{
<T as Queryable>::query(&self.session).await
}
}
```

This means, depending on the type queried, you get a `Vec<Model>`, or a single `Model`.

E.g.
```rs
let fissures: Vec<Fissure> = client.fetch<Fissure>().await?;
let cetus: Cetus = client.fetch<Cetus>().await?;
```

If you have any questions, feel free to ask on the discord, or open an issue.
21 changes: 11 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ license = "MIT"
[features]
default = ["worldstate"]

full = ["worldstate_full", "market_full"]
worldstate = []
multilangual = ["worldstate"]
worldstate_listeners = ["worldstate"]
Expand All @@ -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"] }
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![doc = include_str!("../README.md")]

#[cfg(feature = "worldstate")]
#[forbid(missing_docs)]
pub mod worldstate;

#[cfg(feature = "market")]
Expand Down
27 changes: 24 additions & 3 deletions src/market/client.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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<StatisticItem>),
/// ItemInfo
ItemInfo(Arc<ItemInfo>),
/// Items
Items(Arc<Vec<Item>>),
/// Orders
Orders(Arc<Vec<Order>>),
}

/// The client
#[derive(Debug, Clone)]
#[cfg_attr(not(feature = "market_cache"), derive(Default))]
pub struct Client {
Expand All @@ -28,6 +37,7 @@ pub struct Client {
}

impl Client {
/// Creates a new [Client]
pub fn new() -> Self {
Default::default()
}
Expand Down Expand Up @@ -62,7 +72,9 @@ impl Client {
.await?;

if response.status().is_success() {
let json_result = response.json::<ItemInfoPayload>().await?;
let json_result = response
.json::<crate::market::models::ItemInfoPayload>()
.await?;
Ok(json_result.payload.item)
} else {
Err(response.status().into())
Expand All @@ -78,7 +90,9 @@ impl Client {
.await?;

if response.status().is_success() {
let json_result = response.json::<ItemsPayload>().await?;
let json_result = response
.json::<crate::market::models::ItemsPayload>()
.await?;
Ok(json_result.payload.items)
} else {
Err(response.status().into())
Expand All @@ -96,14 +110,17 @@ impl Client {
.await?;

if response.status().is_success() {
let json_result = response.json::<OrderPayload>().await?;
let json_result = response
.json::<crate::market::models::OrderPayload>()
.await?;
Ok(json_result.payload.orders)
} else {
Err(response.status().into())
}
}
}

/// The cached version of the client
#[cfg(feature = "market_cache")]
pub mod cached {
use {
Expand All @@ -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<Response, ApiError>),
}

#[cfg(feature = "market_cache")]
impl Client {
/// Creates a new client with a custom cache
pub fn new_with_cache(cache: Cache<String, CacheValue>) -> Self {
Self {
session: Default::default(),
Expand Down
6 changes: 6 additions & 0 deletions src/market/error.rs
Original file line number Diff line number Diff line change
@@ -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),
}
Expand Down
2 changes: 2 additions & 0 deletions src/market/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions src/market/models/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
6 changes: 6 additions & 0 deletions src/market/models/item_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<serde_json::Value>>, // seems to be empty all the time
}
Expand Down
13 changes: 9 additions & 4 deletions src/market/models/mod.rs
Original file line number Diff line number Diff line change
@@ -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::*;
8 changes: 1 addition & 7 deletions src/market/models/statistic_item.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use {
super::OrderType,
chrono::{DateTime, Utc},
serde::{Deserialize, Serialize},
};
Expand Down Expand Up @@ -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};
Expand Down
Loading

0 comments on commit 03c54d1

Please sign in to comment.