diff --git a/src/api.rs b/src/api.rs index e280280..c24b04c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -3,23 +3,71 @@ use reqwest::header; use std::fmt::Debug; use strum_macros::EnumString; -/// All methods contain an `Option` to provide an alternate api key to use if it differs from the default -pub struct OpenShockAPI { - client: reqwest::Client, - base_url: String, - default_key: String, +/// Builder for [`OpenShockAPI`] +#[derive(Default)] +pub struct OpenShockAPIBuilder { + base_url: Option, + default_key: Option, + app_name: Option, + app_version: Option, } -/// Which list of shockers to return -#[derive(EnumString, Debug)] -pub enum ListShockerSource { - Own, - Shared, -} +impl OpenShockAPIBuilder { + /// Create a new builder + pub fn new() -> Self { + Self::default() + } + + /// set the base URL to use + /// + /// this is optional and can be provided to use a self-hosted instance of the OpenShock API. if + /// left unset, the default (`https://api.openshock.app`) will be used. + pub fn with_base_url(mut self, base_url: String) -> Self { + self.base_url = Some(base_url); + self + } + + /// set the API token to use + /// + /// this must be provided + pub fn with_default_api_token(mut self, default_api_token: String) -> Self { + self.default_key = Some(default_api_token); + self + } + + /// set the name and optionally version of the app using this crate + /// + /// this is optional. if provided, the information will be added to the user agent string for + /// all OpenShock API requests and also sent in [`OpenShockAPI::post_control`] so the app name + /// shows up in the OpenShock log. + pub fn with_app(mut self, app_name: String, app_version: Option) -> Self { + self.app_name = Some(app_name); + self.app_version = app_version; + self + } + + /// check parameters and build an instance of [`OpenShockAPI`] + pub fn build(self) -> Result { + let base_url = self + .base_url + .unwrap_or("https://api.openshock.app".to_string()); + let Some(default_key) = self.default_key else { + return Err(Error::MissingApiToken); + }; + + let mut user_agent = format!("rzap/{}", env!("CARGO_PKG_VERSION")); + // maybe add platform information as well? + let app_name = if let Some(app_name) = self.app_name { + if let Some(app_version) = self.app_version { + user_agent += &format!(" ({} {})", app_name, app_version); + } else { + user_agent += &format!(" ({})", app_name); + } + app_name + } else { + "rzap".to_string() + }; -impl OpenShockAPI { - /// Create a new instance of the api interface with a default key and the base_url, because OpenShock can be self hosted `base_url` can be any url without the leading `/` if `None` is provided the default of is used. - pub fn new(base_url: Option, default_key: String) -> Self { let mut headers = header::HeaderMap::new(); headers.insert( "Content-type", @@ -29,16 +77,54 @@ impl OpenShockAPI { "accept", header::HeaderValue::from_static("application/json"), ); + headers.insert( + header::USER_AGENT, + header::HeaderValue::from_str(&user_agent).map_err(|e| Error::InvalidHeaderValue(e))?, + ); let client = reqwest::Client::builder() .default_headers(headers) .build() .unwrap(); - let base_url = base_url.unwrap_or("https://api.openshock.app".to_string()); - OpenShockAPI { + + Ok(OpenShockAPI { client, base_url, default_key, + app_name, + }) + } +} + +/// All methods contain an `Option` to provide an alternate api key to use if it differs from the default +pub struct OpenShockAPI { + client: reqwest::Client, + base_url: String, + default_key: String, + app_name: String, +} + +/// Which list of shockers to return +#[derive(EnumString, Debug)] +pub enum ListShockerSource { + Own, + Shared, +} + +impl OpenShockAPI { + /// Return a builder for the api interface + /// + /// this is the same as [`OpenShockAPIBuilder::new`] + pub fn builder() -> OpenShockAPIBuilder { + OpenShockAPIBuilder::new() + } + + /// Create a new instance of the api interface with a default key and the base_url, because OpenShock can be self hosted `base_url` can be any url without the leading `/` if `None` is provided the default of is used. + pub fn new(base_url: Option, default_key: String) -> Self { + let mut builder = Self::builder().with_default_api_token(default_key); + if let Some(base_url) = base_url { + builder = builder.with_base_url(base_url); } + builder.build().unwrap() } /// Gets user info from the provided API key, the default key from the instance is used if `None` is provided @@ -108,7 +194,7 @@ impl OpenShockAPI { duration: duration, exclusive: true, }], - custom_name: "rusty".to_string(), + custom_name: self.app_name.clone(), })?; let resp = self diff --git a/src/error.rs b/src/error.rs index e439aae..f35bb5b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,14 @@ /// Error type for functions in this crate #[derive(Debug)] pub enum Error { + /// error propagated from reqwest Reqwest(reqwest::Error), + /// error propagated from serde Serde(serde_json::Error), + /// no API token was provided to the API interface + MissingApiToken, + /// invalid header value when building the API interface + InvalidHeaderValue(reqwest::header::InvalidHeaderValue), } impl From for Error { @@ -22,6 +28,8 @@ impl std::fmt::Display for Error { match self { Self::Reqwest(e) => e.fmt(f), Self::Serde(e) => e.fmt(f), + Self::MissingApiToken => write!(f, "no API token was provided"), + Self::InvalidHeaderValue(e) => write!(f, "invalid header value for user agent: {}", e), } } } @@ -31,6 +39,8 @@ impl std::error::Error for Error { match self { Self::Reqwest(e) => e.source(), Self::Serde(e) => e.source(), + Self::MissingApiToken => None, + Self::InvalidHeaderValue(e) => e.source(), } } }