lemmy-client 1.0.5

A Rust HTTP client for Lemmy.
Documentation
use crate::{
    form::{LemmyForm, LemmyRequest},
    lemmy_client_trait::{private_trait, LemmyClientInternal},
    response::{LemmyResponse, LemmyResult},
    utils::ClientOptions,
};
use cfg_if::cfg_if;
use http::Method;
use lemmy_api_common::LemmyErrorType;
use std::collections::HashMap;

trait WithHeaders {
    fn with_headers(self, headers: &HashMap<String, String>) -> Self;
}

trait MaybeWithJwt {
    fn maybe_with_jwt(self, jwt: Option<String>) -> Self;
}

fn build_route(route: &str, ClientOptions { domain, secure }: &ClientOptions) -> String {
    format!(
        "http{}://{domain}/api/v3/{route}",
        if *secure { "s" } else { "" }
    )
}

fn map_other_error<E: ToString>(e: E) -> LemmyErrorType {
    LemmyErrorType::Unknown(e.to_string())
}

fn deserialize_response<Response: LemmyResponse>(res: &str) -> Result<Response, LemmyErrorType> {
    serde_json::from_str::<Response>(res)
        .map_err(|_| serde_json::from_str::<LemmyErrorType>(res).unwrap_or_else(map_other_error))
}

cfg_if! {
  if #[cfg(target_family = "wasm")] {
    use gloo_net::http::{Request, RequestBuilder};
    use web_sys::wasm_bindgen::UnwrapThrowExt;
    pub struct Fetch(pub ClientOptions);

    impl Fetch {
        pub fn new(options: ClientOptions) -> Self {
            Self(options)
        }

        fn build_fetch_query<T: serde::Serialize>(&self, path: &str, form: &T) -> String {
            let form_str = serde_urlencoded::to_string(form).unwrap_or_else(|_| path.to_string());
            format!("{}?{}", build_route(path, &self.0), form_str)
        }
    }

    impl WithHeaders for RequestBuilder {
        fn with_headers(self, headers: &HashMap<String, String>) -> Self {
            headers.iter().fold(self, |acc, (header, value)| acc.header(header.as_str(), value.as_str()))
        }
    }

    impl MaybeWithJwt for RequestBuilder {
        fn maybe_with_jwt(self, jwt: Option<String>) -> Self {
            if let Some(jwt) = jwt {
                self.header(http::header::AUTHORIZATION.as_str(), format!("Bearer {jwt}").as_str())
            } else {
                self
            }
        }
    }

    impl private_trait::LemmyClientInternal for Fetch {
      async fn make_request<Response, Form>(
                &self,
                method: Method,
                path: &str,
                request: LemmyRequest<Form>,
                headers: &HashMap<String, String>
            ) -> LemmyResult<Response>
            where
                Response: LemmyResponse,
                Form: LemmyForm,
            {
                let route = &build_route(path, &self.0);
                let LemmyRequest { body, jwt } = request;

                #[allow(unused_mut)]
                let mut req = match method {
                    Method::GET => Request::get(&self.build_fetch_query(path, &body)),
                    Method::POST => Request::post(route),
                    Method::PUT => Request::put(route),
                    method => unreachable!("This crate only uses GET, POST, and PUT HTTP methods. Got {method:?}")
                }.with_headers(headers)
                 .maybe_with_jwt(jwt);

                let res = match method {
                    Method::GET => req.build().expect_throw("Could not parse query params"),
                    Method::POST | Method::PUT => req.json(&body).expect_throw("Could not parse JSON body"),
                    method => unreachable!("This crate only uses GET, POST, and PUT HTTP methods. Got {method:?}")
                }.send()
                 .await
                 .map_err(map_other_error)?
                 .text()
                 .await
                 .map_err(map_other_error)?;

                deserialize_response(&res)
        }
    }

    impl LemmyClientInternal for Fetch {}
  } else {
        impl WithHeaders for reqwest::RequestBuilder {
            fn with_headers(self, headers: &HashMap<String, String>) -> Self {
                let mut client = headers.iter().fold(self, |acc, (header, value)| acc.header(header, value));

                if !headers.keys().any(|key| key.eq_ignore_ascii_case("user-agent")) {
                    client = client.header("user-agent", "Lemmy-Client-rs/0.19.4-alpha.1");
                }

                client
            }
        }

      impl MaybeWithJwt for reqwest::RequestBuilder {
          fn maybe_with_jwt(self, jwt: Option<String>) -> Self {
              if let Some(jwt) = jwt {
                  self.bearer_auth(jwt)
              } else {
                  self
              }
          }
      }

      pub struct ClientWrapper {
          client: reqwest::Client,
          pub options: ClientOptions
      }

      impl ClientWrapper {
          pub fn new(options: ClientOptions) -> Self {
              Self {
                  client: reqwest::Client::new(),
                  options
              }
          }
      }

      impl private_trait::LemmyClientInternal for ClientWrapper {
            async fn make_request<Response, Form>(
                &self,
                method: Method,
                path: &str,
                request: LemmyRequest<Form>,
                headers: &HashMap<String, String>
            ) -> LemmyResult<Response>
            where
                Response: LemmyResponse,
                Form: LemmyForm,
            {
                let route = build_route(path, &self.options);
                let LemmyRequest { body, jwt } = request;

                let res = match method {
                    Method::GET =>
                        self
                            .client
                            .get(route)
                            .with_headers(headers)
                            .maybe_with_jwt(jwt)
                            .query(&body),
                    Method::POST =>
                        self
                            .client
                            .post(route)
                            .with_headers(headers)
                            .maybe_with_jwt(jwt)
                            .json(&body),
                    Method::PUT =>
                        self
                            .client
                            .put(route)
                            .with_headers(headers)
                            .maybe_with_jwt(jwt)
                            .json(&body),
                    _ => unreachable!("This crate does not use other HTTP methods.")
                }.send()
                 .await
                 .map_err(map_other_error)?
                 .text()
                 .await
                 .map_err(map_other_error)?;

                deserialize_response(&res)
            }
        }

      impl LemmyClientInternal for ClientWrapper {}
  }
}