use serde::Deserialize;
use thiserror::Error;
use crate::{board::BoardData, Vestaboard};
const LOCAL_ENABLEMENT_TOKEN_HEADER: &str = "X-Vestaboard-Local-Api-Enablement-Token";
const LOCAL_API_KEY_HEADER: &str = "X-Vestaboard-Local-Api-Key";
const LOCAL_DEVICE_PORT: u16 = 7000;
const LOCAL_API_ENABLEMENT_URI: &str = "/local-api/enablement";
const LOCAL_API_MESSAGE_URI: &str = "/local-api/message";
#[derive(Debug, Clone)]
pub struct LocalConfig {
pub api_key: String,
pub ip_address: std::net::IpAddr,
}
impl<const ROWS: usize, const COLS: usize> Vestaboard<LocalConfig, ROWS, COLS> {
pub fn new_local_api(config: LocalConfig) -> Self {
use std::str::FromStr;
let headers = reqwest::header::HeaderMap::from_iter([
(
reqwest::header::CONTENT_TYPE,
reqwest::header::HeaderValue::from_static("application/json"),
),
(
reqwest::header::HeaderName::from_str(LOCAL_API_KEY_HEADER).unwrap(),
reqwest::header::HeaderValue::from_str(&config.api_key).expect("failed to parse api key"),
),
]);
Vestaboard {
client: reqwest::Client::builder()
.default_headers(headers)
.user_agent(format!("vestaboard-rs/{}", env!("CARGO_PKG_VERSION")))
.build()
.expect("failed to build reqwest client"),
config,
}
}
pub async fn read(&self) -> Result<BoardData<ROWS, COLS>, LocalApiError> {
let url = format!(
"http://{}:{}{}",
self.config.ip_address, LOCAL_DEVICE_PORT, LOCAL_API_MESSAGE_URI
);
let res = self.client.get(url).send().await?;
if !res.status().is_success() {
return Err(LocalApiError::ApiError(res.text().await?));
}
Ok(res.json().await?)
}
pub async fn write(&self, message: BoardData<ROWS, COLS>) -> Result<(), LocalApiError> {
let url = format!(
"http://{}:{}{}",
self.config.ip_address, LOCAL_DEVICE_PORT, LOCAL_API_MESSAGE_URI
);
let res = self.client.post(url).json(&message).send().await?;
if !res.status().is_success() {
Err(LocalApiError::ApiError(res.text().await?))
} else {
Ok(())
}
}
pub async fn get_local_api_key(
ip_address: Option<std::net::IpAddr>,
local_enablement_token: Option<String>,
) -> Result<String, LocalApiError> {
let token = if let Some(token) = local_enablement_token {
token
} else if let Ok(token) = std::env::var("LOCAL_ENABLEMENT_TOKEN") {
token
} else {
return Err(LocalApiError::MissingHeader {
name: "local_enablement_token".to_string(),
env_var: "LOCAL_ENABLEMENT_TOKEN".to_string(),
});
};
let ip = if let Some(ip) = ip_address {
ip
} else if let Ok(ip) = std::env::var("LOCAL_DEVICE_IP") {
if let Ok(ip) = ip.parse::<std::net::IpAddr>() {
ip
} else {
return Err(LocalApiError::InvalidIp);
}
} else {
return Err(LocalApiError::MissingHeader {
name: "device_ip".to_string(),
env_var: "LOCAL_DEVICE_IP".to_string(),
});
};
let headers = reqwest::header::HeaderMap::from_iter([
(
reqwest::header::CONTENT_TYPE,
reqwest::header::HeaderValue::from_static("application/json"),
),
(
reqwest::header::HeaderName::from_static(LOCAL_ENABLEMENT_TOKEN_HEADER),
reqwest::header::HeaderValue::from_str(&token).expect("failed to parse local enablement token"),
),
]);
let client = reqwest::Client::builder()
.default_headers(headers)
.user_agent(format!("vestaboard-rs/{}", env!("CARGO_PKG_VERSION")))
.build()
.expect("failed to build reqwest client");
let url = format!("http://{}:{}{}", ip, LOCAL_DEVICE_PORT, LOCAL_API_ENABLEMENT_URI);
let res = client.post(url).send().await?;
let body: LocalApiEnablementResponse = res.json().await?;
if let Some(api_key) = body.api_key {
Ok(api_key)
} else {
Err(LocalApiError::ApiError(body.message))
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LocalApiEnablementResponse {
message: String,
api_key: Option<String>,
}
#[derive(Error, Debug)]
pub enum LocalApiError {
#[error("reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),
#[error("missing header `{name:?}`. pass the value or set the `{env_var:?}` environment variable.")]
MissingHeader { name: String, env_var: String },
#[error("invalid ip address for local device")]
InvalidIp,
#[error("api error: {0}")]
ApiError(String),
}