Skip to main content

workflowy_api/
client.rs

1use crate::{GetNodesRequest, GetNodesResponse};
2use crate::{Key, Limiter};
3use derive_more::{From, Into};
4use errgonomic::handle;
5use reqwest::Client as HttpClient;
6use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
7use secrecy::ExposeSecret as _;
8use serde::de::DeserializeOwned;
9use std::sync::LazyLock;
10use thiserror::Error;
11use url::Url;
12use url_macro::url;
13
14// The trailing slash is required for Url::join to work properly
15pub static BASE_URL: LazyLock<Url> = LazyLock::new(|| url!("https://workflowy.com/api/v1/"));
16
17#[derive(From, Into, Clone, Debug)]
18pub struct Client {
19    pub inner: HttpClient,
20    pub base: Url,
21    pub limiter: Limiter,
22}
23
24impl Client {
25    const NODES_PATH: &str = "nodes";
26
27    pub fn new(key: impl Into<Key>) -> Result<Self, ClientNewError> {
28        use ClientNewError::*;
29        let key = key.into();
30        Ok(handle!(Self::try_from(key), TryFromKeyFailed))
31    }
32
33    pub async fn get_nodes(&self, request: &GetNodesRequest<'_>) -> Result<GetNodesResponse, ClientGetNodesError> {
34        use ClientGetNodesError::*;
35        let url = self
36            .base
37            .join(Self::NODES_PATH)
38            .expect("always succeeds because `NODES_PATH` is a valid relative URL path");
39        let result = self.inner.get(url).query(&request).send().await;
40        let response = handle!(result, SendFailed);
41        let nodes = handle!(Self::handle(response).await, HandleFailed);
42        Ok(nodes)
43    }
44
45    pub async fn handle<T>(response: reqwest::Response) -> Result<T, HandleError>
46    where
47        T: DeserializeOwned,
48    {
49        use HandleError::*;
50        let status = response.status();
51        if status.is_success() {
52            let response = response.json::<T>().await;
53            Ok(handle!(response, DecodeResponseFailed))
54        } else {
55            let body = response.text().await;
56            let body = handle!(body, ReadBodyFailed, status);
57            Err(CheckStatusFailed {
58                status,
59                body,
60            })
61        }
62    }
63}
64
65#[derive(Error, Debug)]
66pub enum HandleError {
67    #[error("Workflowy returned status '{status}': '{body}'")]
68    CheckStatusFailed { status: reqwest::StatusCode, body: String },
69    #[error("failed to read Workflowy error response body for status '{status}'")]
70    ReadBodyFailed { source: reqwest::Error, status: reqwest::StatusCode },
71    #[error("failed to decode Workflowy response")]
72    DecodeResponseFailed { source: reqwest::Error },
73}
74
75#[derive(Error, Debug)]
76pub enum ClientNewError {
77    #[error("failed to create Workflowy API client from key")]
78    TryFromKeyFailed { source: ConvertKeyToClientError },
79}
80
81impl From<HttpClient> for Client {
82    fn from(inner: HttpClient) -> Self {
83        Self {
84            inner,
85            base: BASE_URL.clone(),
86            limiter: Limiter,
87        }
88    }
89}
90
91impl TryFrom<Key> for Client {
92    type Error = ConvertKeyToClientError;
93
94    fn try_from(key: Key) -> Result<Self, Self::Error> {
95        use ConvertKeyToClientError::*;
96        let header_value_raw = format!("Bearer {}", key.expose_secret());
97        let mut header_value = handle!(HeaderValue::try_from(header_value_raw), HeaderValueTryFromFailed, key);
98        header_value.set_sensitive(true);
99        let mut headers = HeaderMap::new();
100        headers.insert(AUTHORIZATION, header_value);
101        let inner = HttpClient::builder().default_headers(headers).build();
102        let inner = handle!(inner, BuildHttpClientFailed);
103        Ok(Self::from(inner))
104    }
105}
106
107#[derive(Error, Debug)]
108pub enum ConvertKeyToClientError {
109    #[error("failed to convert Workflowy API key into an authorization header value")]
110    HeaderValueTryFromFailed { source: reqwest::header::InvalidHeaderValue, key: Key },
111    #[error("failed to build HTTP client")]
112    BuildHttpClientFailed { source: reqwest::Error },
113}
114
115#[derive(Error, Debug)]
116pub enum ClientGetNodesError {
117    #[error("failed to send get nodes request")]
118    SendFailed { source: reqwest::Error },
119    #[error("failed to handle get nodes response")]
120    HandleFailed { source: HandleError },
121}