use std::{collections::HashSet, sync::Arc};
use anyhow::Result;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::{debug, error};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RighGravityNodeState {
pub id: String,
pub ip: Option<String>,
pub ipv6: Option<HashSet<String>>,
pub firmware_version: Option<String>,
pub onboarding_state: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RighGravityLocation {
pub location_id: String,
}
#[derive(Clone)]
pub struct RighGravityClient {
client: Client,
base_url: String,
location_id: Arc<RwLock<Option<String>>>,
}
impl RighGravityClient {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
client: Client::new(),
base_url: base_url.into(),
location_id: Arc::new(RwLock::new(None)),
}
}
pub async fn fetch_node_states(&self) -> Result<Vec<RighGravityNodeState>> {
let mut location_id = self.location_id.read().await.clone();
if location_id.is_none() {
let url = format!("{}/api/locations", self.base_url);
let response = self.client.get(&url).send().await?;
if response.status().is_success() {
let locations: Vec<RighGravityLocation> = response.json().await?;
if !locations.is_empty() {
location_id = Some(locations[0].location_id.clone());
let mut loc = self.location_id.write().await;
*loc = location_id.clone();
}
}
}
let location_id =
location_id.ok_or_else(|| anyhow::anyhow!("No location available in RighGravity"))?;
let url = format!("{}/api/locations/{}/nodes", self.base_url, location_id);
debug!("Fetching node states from RighGravity: {}", url);
let response = self.client.get(&url).send().await?;
if !response.status().is_success() {
error!("Failed to fetch node states: status={}", response.status());
return Err(anyhow::anyhow!(
"Failed to fetch node states: {}",
response.status()
));
}
let response_body: serde_json::Value = response.json().await?;
let nodes: Vec<RighGravityNodeState> = if let Some(nodes_array) = response_body.get("nodes")
{
serde_json::from_value(nodes_array.clone())?
} else {
serde_json::from_value(response_body)?
};
debug!("Fetched {} node states from RighGravity", nodes.len());
Ok(nodes)
}
pub fn is_node_online(node: &RighGravityNodeState) -> bool {
!matches!(
node.onboarding_state.as_str(),
"onboarding_wait_onboarding" | "onboarding_disconnected" | "disconnected"
)
}
pub async fn get_node_by_id(&self, node_id: &str) -> Result<Option<RighGravityNodeState>> {
let nodes = self.fetch_node_states().await?;
Ok(nodes.into_iter().find(|node| node.id == node_id))
}
}