Skip to content

Commit

Permalink
Refactoring things (#3)
Browse files Browse the repository at this point in the history
* feat: convert functions to methods

* refactor: use function for response parsing

* docs: changelog
  • Loading branch information
bellackn committed May 1, 2024
1 parent db3c08a commit 4135d11
Show file tree
Hide file tree
Showing 14 changed files with 476 additions and 357 deletions.
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.0] - 2024-05-01

### Added

- You can now use `SteamClient::new()` to create a client instance without an API key
- To use an API key, you can take the newly added `SteamClient::from(key)` method
- Dependency `log` to log a warning if you use a `SteamClient` without a valid API key
- New `client.parse_request()` function to reduce code duplication
- `Default` implementations for `SteamClient` and some structs to work with `parse_request`

### Changed

- What were functions previously are now methods of `SteamClient`
- E.g. `steamr::friends::get_friends()` is now `client.get_friends()`
- Module structure of the crate

### Removed

* `#![deny(rustdoc::missing_doc_code_examples)]` directive (see [issue #101730](https://github.com/rust-lang/rust/issues/101730) for details)
- `#![deny(rustdoc::missing_doc_code_examples)]` directive (see [issue #101730](https://github.com/rust-lang/rust/issues/101730) for details)
- `ApiClient` trait - it didn't add any use

## [0.3.1] - 2024-03-25

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ license = "Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
log = "0.4.21"
reqwest = {version = "0.12.1", features = ["blocking", "json"] }
serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108"
Expand Down
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ build:
@ cargo build

doc:
@ cargo doc --no-deps
@ cargo doc --no-deps --open

# Publish the crate on crates.io
release version: lint test
Expand Down
36 changes: 28 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,49 @@ It uses the [reqwest](https://github.com/seanmonstar/reqwest) crate under the ho

## Requirements

You need a valid API key to use this library. Visit <https://steamcommunity.com/dev/apikey> to obtain yours.
You need a valid API key to make full use of this library. Visit <https://steamcommunity.com/dev/apikey> to obtain yours.

## Example

```rust,no_run
use steamr::client::SteamClient;
use steamr::errors::SteamError;
use steamr::games::get_owned_games;
fn main() -> Result<(), SteamError> {
// Create a new client that will be used to communicate with Steam's API.
// Create a new client that will be used to communicate with Steam's API.
let api_key = String::from("your-api-key");
let steam_client = SteamClient::new(api_key);
let steam_client = SteamClient::from(api_key);
// Get a list of all games from the user with the provided Steam ID (given that they are publicly visible)
let steam_id = "some-steam-id";
let steam_lib = get_owned_games(&steam_client, &steam_id)?;
let steam_lib = steam_client.get_library(&steam_id)?;
// Print out the games that were played for more than an hour.
steam_lib.games.iter()
.filter(|g| g.playtime_forever > 60)
.for_each(|g| println!("{}", g.name));
Ok(())
}
```

There are some endpoints that don't require an API key. If you only need those,
you can use `SteamClient` like so:

```rust,no_run
use steamr::client::SteamClient;
use steamr::errors::SteamError;
fn main() -> Result<(), SteamError> {
// Create a new SteamClient without an API key
let steam_client = SteamClient::new();
// Get news for a game
let app_id = "10";
let news = steam_client.get_game_news(app_id, 5, 100)?;
news.game_news.iter()
.for_each(|n| println!("The article '{}' was written by '{}'", n.title, n.author));
Ok(())
}
```
61 changes: 42 additions & 19 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
//! The Steam API client

use crate::errors::SteamError;
use log::warn;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use serde::Serialize;
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;

/// Common functions for a Steam API client.
pub trait ApiClient {
/// Send a GET request and return JSON
fn get_request<T: Serialize>(
&self,
endpoint: &str,
query: Vec<(&str, T)>,
) -> Result<Value, SteamError>;
}

/// This struct holds the blocking reqwest client and is used to interact with the API.
pub struct SteamClient {
/// A [`reqwest::blocking`] HTTP client
Expand All @@ -24,12 +15,40 @@ pub struct SteamClient {
api_key: String,
}

impl ApiClient for SteamClient {
fn get_request<T: Serialize>(
impl Default for SteamClient {
fn default() -> Self {
let client = reqwest::blocking::Client::new();
Self {
client,
api_key: String::new(),
}
}
}

impl SteamClient {
/// Returns a new SteamClient instance carrying a developer API token
pub fn from(api_key: String) -> Self {
let client = reqwest::blocking::Client::new();
Self { client, api_key }
}

/// Return a SteamClient without a Steam API token
pub fn new() -> Self {
let client = reqwest::blocking::Client::new();
Self {
client,
api_key: String::new(),
}
}

pub(crate) fn get_request<T: Serialize>(
&self,
endpoint: &str,
query: Vec<(&str, T)>,
) -> Result<Value, SteamError> {
if self.api_key.is_empty() {
warn!("Not using a valid API key. Is this on purpose?")
}
let response = self
.client
.get(endpoint)
Expand All @@ -52,12 +71,16 @@ impl ApiClient for SteamClient {
)),
}
}
}

impl SteamClient {
/// Returns a new SteamClient instance.
pub fn new(api_key: String) -> Self {
let client = reqwest::blocking::Client::new();
SteamClient { client, api_key }
pub(crate) fn parse_response<R: DeserializeOwned, S: From<R> + DeserializeOwned>(
&self,
response: Value,
) -> Result<S, SteamError> {
let res = serde_json::from_value::<R>(response);
if let Ok(v) = res {
Ok(v.into())
} else {
Err(SteamError::NoData)
}
}
}
1 change: 0 additions & 1 deletion src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ pub enum SteamError {
NoData,
}

#[doc(hidden)]
impl From<reqwest::Error> for SteamError {
fn from(err: Error) -> Self {
// If the reqwest goes wrong, we should forward it to the user
Expand Down
97 changes: 51 additions & 46 deletions src/friends.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Functions to handle any friends-related data
//! Functionality dealing with an account's friends

use crate::client::{ApiClient, SteamClient};
use crate::client::SteamClient;
use crate::errors::SteamError;
use serde::Deserialize;
use std::fmt::Formatter;
Expand All @@ -17,12 +17,19 @@ struct FriendsResponse {
}

/// This is the response that comes from the GetFriendList API.
#[derive(Debug, Deserialize)]
#[derive(Debug, Default, Deserialize)]
pub struct FriendsList {
/// A list of [`Friend`]s
pub friends: Vec<Friend>,
}

impl From<FriendsResponse> for FriendsList {
fn from(value: FriendsResponse) -> Self {
let v = value.response.unwrap_or_default();
Self { friends: v.friends }
}
}

/// Represents a Steam friend and its metadata
#[derive(Debug, Deserialize, PartialEq)]
pub struct Friend {
Expand Down Expand Up @@ -54,49 +61,47 @@ pub enum SteamRelationship {
Friend,
}

/// Gets all friends from the user with the provided Steam ID.
///
/// Example:
///
/// ```no_run
/// # use steamr::client::SteamClient;
/// # use steamr::friends::get_friends;
/// # use steamr::errors::SteamError;
///
/// fn main() -> Result<(), SteamError> {
/// let steam_client = SteamClient::new("an-api-key".to_string());
/// let steam_friends = get_friends(&steam_client, "some-steam-ID")?;
///
/// // Print friends
/// steam_friends.iter().for_each(|f| println!("{}", f));
///
/// Ok(())
/// }
/// ```
///
/// The standard format of "friends since" is the UNIX timestamp, you might want to get a
/// more intuitive time format. You could use the `chrono` crate for this:
/// ```ignore
/// let steam_friends = get_friends(&steam_client, "some-steam-ID")?;
/// steam_friends.iter().for_each(|f| {
/// println!(
/// "me and {} are friends since {}",
/// f.steam_id,
/// chrono::NaiveDateTime::from_timestamp(f.friend_since, 0)
/// )
/// });
/// ```
pub fn get_friends(client: &SteamClient, steam_id: &str) -> Result<Vec<Friend>, SteamError> {
let response = client.get_request(
ENDPOINT_GET_FRIENDLIST,
vec![("steamid", steam_id), ("relationship", "friend")],
)?;

let _res: FriendsResponse =
serde_json::from_value(response).unwrap_or(FriendsResponse { response: None });
impl SteamClient {
/// Gets all friends from the user with the provided Steam ID.
///
/// Example:
///
/// ```no_run
/// # use steamr::client::SteamClient;
/// # use steamr::errors::SteamError;
///
/// fn main() -> Result<(), SteamError> {
/// let steam_client = SteamClient::from("an-api-key".to_string());
/// let steam_friends = steam_client.get_friends("some-steam-ID")?;
///
/// // Print friends
/// steam_friends.iter().for_each(|f| println!("{}", f));
///
/// Ok(())
/// }
/// ```
///
/// The standard format of "friends since" is the UNIX timestamp, you might want to get a
/// more intuitive time format. You could use the `chrono` crate for this:
/// ```ignore
/// let steam_friends = steam_client.get_friends("some-steam-ID")?;
/// steam_friends.iter().for_each(|f| {
/// println!(
/// "me and {} are friends since {}",
/// f.steam_id,
/// chrono::NaiveDateTime::from_timestamp(f.friend_since, 0)
/// )
/// });
/// ```
pub fn get_friends(&self, steam_id: &str) -> Result<Vec<Friend>, SteamError> {
let response = self.get_request(
ENDPOINT_GET_FRIENDLIST,
vec![("steamid", steam_id), ("relationship", "friend")],
)?;

match _res.response {
None => Err(SteamError::NoData),
Some(v) => Ok(v.friends),
let friends_list = self
.parse_response::<FriendsResponse, FriendsList>(response)
.unwrap();
Ok(friends_list.friends)
}
}
Loading

0 comments on commit 4135d11

Please sign in to comment.