1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use reqwest::Client;

use crate::{
    answers::Answer,
    common::{ApiVersion, Info, Response, StackSite, STACK_APP_API},
    questions::Question,
};

#[derive(Debug)]
pub struct StackClient {
    client: Client,
    api_version: ApiVersion,
    stack_site: StackSite,
}

/// StackClient is a client for the Stack Exchange API
///
/// Construct a default client for StackOverflow site, API version 2.3 (https://meta.stackexchange.com/questions/366977/api-2-3-release)
/// ```rust
/// use stack_overflow::client::StackClient;
/// let client = StackClient::new();
/// ```
///
/// Construct a e client for a specific site and API version
/// ```rust
/// use stack_overflow::client::StackClientBuilder;
/// use stack_overflow::common::{ApiVersion, StackSite};
/// let client = StackClientBuilder::new()
///                 .stack_site(StackSite::StackOverflow)
///                 .version(ApiVersion::V2_3)
///                 .build();
/// ```
impl StackClient {
    /// StackClient defaults to StackOverflow as the site and api version 2.3
    pub fn new() -> Self {
        StackClientBuilder::new()
            .version(ApiVersion::V2_3)
            .stack_site(StackSite::StackOverflow)
            .build()
    }

    /// Get info about the client's Stack site. Returns a collection of statistics about the site.
    /// API Docs: https://api.stackexchange.com/docs/info
    pub async fn get_info(
        &self,
    ) -> Result<Response<Info>, Box<dyn std::error::Error>> {
        let url = format!(
            "{}/{}/info?site={}",
            STACK_APP_API,
            self.api_version.to_string(),
            self.stack_site.to_string()
        );
        let resp = self.client.get(&url).send().await?.text().await?;
        let deserialized: Response<Info> = serde_json::from_str(&resp.as_str())
            .expect("unable to deserialize response");
        Ok(deserialized)
    }

    /// Get featured questions for a tag.
    /// Note that this only retrieves the first page of results.
    /// API Docs: https://api.stackexchange.com/docs/featured-questions
    pub async fn get_featured_questions(
        &self,
        tag: &str,
    ) -> Result<Response<Question>, Box<dyn std::error::Error>> {
        self.get_featured_questions_paginated(tag, 1).await
    }

    /// Get featured questions for a tag and explicitly provide a page.
    /// Note that pagination is not supported beyond 25 pages because authentication is required.
    /// API Docs: https://api.stackexchange.com/docs/featured-questions
    pub async fn get_featured_questions_paginated(
        &self,
        tag: &str,
        page: u32,
    ) -> Result<Response<Question>, Box<dyn std::error::Error>> {
        let url = format!(
            "{}/{}/questions/featured?tagged={}&site={}&page={}",
            STACK_APP_API,
            self.api_version.to_string(),
            tag,
            self.stack_site.to_string(),
            page
        );

        let resp = self.client.get(&url).send().await?.text().await?;
        let deserialized: Response<Question> = serde_json::from_str(
            &resp.as_str(),
        )
        .expect(format!("Failed to deserialized response: {}", resp).as_str());
        Ok(deserialized)
    }

    /// Get answers provided by a user
    /// API Docs: https://api.stackexchange.com/docs/answers-on-users
    pub async fn get_answers_for_user(
        &self,
        user_id: u32,
        page: Option<u32>,
    ) -> Result<Response<Answer>, Box<dyn std::error::Error>> {
        let url: String = format!(
            "{}/{}/users/{}/answers?order=desc&sort=activity&site={}&page={}",
            STACK_APP_API,
            self.api_version.to_string(),
            user_id.to_string(),
            self.stack_site.to_string(),
            page.unwrap_or(1),
        );

        let resp = self.client.get(&url).send().await?.text().await?;
        let deserialized: Response<Answer> = serde_json::from_str(
            resp.as_str(),
        )
        .expect(format!("Failed to deserialize response: {}", resp).as_str());
        Ok(deserialized)
    }
}
pub struct StackClientBuilder {
    api_version: Option<ApiVersion>,
    stack_site: Option<StackSite>,
}

impl StackClientBuilder {
    pub fn new() -> Self {
        StackClientBuilder {
            api_version: None,
            stack_site: None,
        }
    }

    pub fn version(mut self, version: ApiVersion) -> Self {
        self.api_version = Some(version);
        self
    }

    pub fn stack_site(mut self, site: StackSite) -> Self {
        self.stack_site = Some(site);
        self
    }

    pub fn build(self) -> StackClient {
        StackClient {
            api_version: self.api_version.unwrap_or(ApiVersion::V2_3),
            stack_site: self.stack_site.unwrap_or(StackSite::StackOverflow),
            client: reqwest::Client::builder()
                .gzip(true) // Stack api responses are gzip encoded: https://api.stackexchange.com/docs/compression
                .build()
                .expect("unable to create reqwest http client"),
        }
    }
}