hacker_rs/
client.rs

1//! Hacker News API client bindings and various methods for interacting.
2
3use crate::{
4    comments::HackerNewsComment, errors::HackerNewsResult, items::HackerNewsItem,
5    stories::HackerNewsStory, users::HackerNewsUser, HackerNewsID, API_BASE_URL,
6    DEFAULT_TIMEOUT_SECONDS, ITEM_ENDPOINT, USERS_ENDPOINT,
7};
8
9/// Version information for the Hacker News API containing the base URLs.
10#[derive(Debug, Clone, Copy)]
11pub enum ApiVersion {
12    /// Represents version 0 of the Hacker News API.
13    V0,
14}
15
16/// A wrapping HTTP client for Hacker News Firebase API and real-time data.
17/// A client instance should only be instantiated once in an application's
18/// lifecycle, seeking to reuse it where possible.
19#[derive(Debug)]
20pub struct HackerNewsClient {
21    /// An internal HTTP client to be used for connections to the Hacker News API.
22    http_client: reqwest::Client,
23    /// The internal version of the Hacker News API your client will target.
24    version: ApiVersion,
25}
26
27impl Default for HackerNewsClient {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl HackerNewsClient {
34    /// Internally constructs the client allowing for flexibility in configuring the timeout.
35    fn new_client(timeout: u64) -> Self {
36        let client = reqwest::ClientBuilder::new()
37            .timeout(std::time::Duration::from_secs(timeout))
38            .build()
39            .unwrap();
40
41        Self {
42            http_client: client,
43            version: ApiVersion::V0,
44        }
45    }
46
47    /// Constructs a new client pointing to the latest Hacker News API version.
48    pub fn new() -> Self {
49        Self::new_client(DEFAULT_TIMEOUT_SECONDS)
50    }
51
52    /// Constructs a new client pointing to the latest Hacker News API version with the configured request timeout.
53    pub fn new_with_timeout(timeout: u64) -> Self {
54        Self::new_client(timeout)
55    }
56
57    /// Constructs the base URL including the version.
58    fn versioned_api_base_url(&self) -> String {
59        let version = match self.version {
60            ApiVersion::V0 => "v0",
61        };
62
63        format!("{}/{}", API_BASE_URL, version)
64    }
65
66    /// Retrieves item information based on the given ID.
67    pub async fn get_item(&self, id: HackerNewsID) -> HackerNewsResult<HackerNewsItem> {
68        let item = self
69            .http_client
70            .get(format!(
71                "{}/{}/{}.json",
72                self.versioned_api_base_url(),
73                ITEM_ENDPOINT,
74                id
75            ))
76            .send()
77            .await?
78            .json::<HackerNewsItem>()
79            .await?;
80
81        Ok(item)
82    }
83
84    /// Retrieves a user from the user endpoint based on the provided username.
85    pub async fn get_user(&self, username: &str) -> HackerNewsResult<HackerNewsUser> {
86        let user = self
87            .http_client
88            .get(format!(
89                "{}/{}/{}.json",
90                self.versioned_api_base_url(),
91                USERS_ENDPOINT,
92                username
93            ))
94            .send()
95            .await?
96            .json::<HackerNewsUser>()
97            .await?;
98
99        Ok(user)
100    }
101
102    /// Retrieves a story from Hacker News, returning errors if the item was not a valid story type.
103    pub async fn get_story(&self, id: HackerNewsID) -> HackerNewsResult<HackerNewsStory> {
104        let item = self.get_item(id).await?;
105        let story = item.try_into()?;
106        Ok(story)
107    }
108
109    /// Retrieves a story comment from Hacker News, returning errors if the item was not a valid comment type.
110    pub async fn get_comment(&self, id: HackerNewsID) -> HackerNewsResult<HackerNewsComment> {
111        let item = self.get_item(id).await?;
112        let comment = item.try_into()?;
113        Ok(comment)
114    }
115}