use std::{path::Path, sync::Arc};
use hyperchad::state::{StatePersistence as _, sqlite::SqlitePersistence};
use moosicbox_app_models::Connection;
use strum::{AsRefStr, EnumString};
use crate::{AppState, AppStateError, UpdateAppState};
#[derive(Debug, Clone, Copy, EnumString, AsRefStr)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum PersistenceKey {
ConnectionId,
ConnectionName,
Connection,
Connections,
DefaultDownloadLocation,
}
impl From<PersistenceKey> for String {
fn from(value: PersistenceKey) -> Self {
value.to_string()
}
}
impl std::fmt::Display for PersistenceKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_ref().fmt(f)
}
}
impl AppState {
pub async fn set_persistence(
&mut self,
location: impl AsRef<Path>,
) -> Result<&mut Self, AppStateError> {
*self.persistence.write().await = Some(Arc::new(SqlitePersistence::new(location).await?));
self.init_persistence().await?;
Ok(self)
}
pub async fn with_persistence(
mut self,
location: impl AsRef<Path>,
) -> Result<Self, AppStateError> {
self.set_persistence(location).await?;
Ok(self)
}
pub async fn set_persistence_in_memory(&mut self) -> Result<&mut Self, AppStateError> {
*self.persistence.write().await = Some(Arc::new(SqlitePersistence::new_in_memory().await?));
self.init_persistence().await?;
Ok(self)
}
pub async fn with_persistence_in_memory(mut self) -> Result<Self, AppStateError> {
self.set_persistence_in_memory().await?;
Ok(self)
}
#[must_use]
pub async fn persistence(&self) -> Arc<SqlitePersistence> {
self.persistence.read().await.clone().unwrap()
}
async fn init_persistence(&self) -> Result<(), AppStateError> {
if let Some(connection) = self.get_current_connection().await? {
self.current_connection_updated(&connection).await?;
}
Ok(())
}
pub async fn get_connections(&self) -> Result<Vec<Connection>, AppStateError> {
let persistence = self.persistence().await;
Ok(persistence
.get(PersistenceKey::Connections)
.await?
.unwrap_or_default())
}
pub async fn get_current_connection(&self) -> Result<Option<Connection>, AppStateError> {
let persistence = self.persistence().await;
Ok(persistence.get(PersistenceKey::Connection).await?)
}
pub async fn set_current_connection(
&self,
connection: impl AsRef<Connection>,
) -> Result<(), AppStateError> {
let connection = connection.as_ref();
self.persistence()
.await
.set(PersistenceKey::Connection, connection)
.await?;
self.current_connection_updated(connection).await?;
Ok(())
}
async fn current_connection_updated(
&self,
connection: &Connection,
) -> Result<(), AppStateError> {
use std::collections::BTreeMap;
use moosicbox_music_api::{MusicApi, profiles::PROFILES};
use moosicbox_music_models::ApiSource;
use moosicbox_remote_library::RemoteLibraryMusicApi;
static PROFILE: &str = "master";
let mut apis_map: BTreeMap<ApiSource, Arc<Box<dyn MusicApi>>> = BTreeMap::new();
for api_source in ApiSource::all() {
apis_map.insert(
api_source.clone(),
Arc::new(Box::new(moosicbox_music_api::CachedMusicApi::new(
RemoteLibraryMusicApi::new(
connection.api_url.clone(),
api_source,
PROFILE.to_string(),
),
))),
);
}
PROFILES.upsert(PROFILE.to_string(), Arc::new(apis_map));
self.set_state(UpdateAppState {
api_url: Some(Some(connection.api_url.clone())),
..Default::default()
})
.await?;
Ok(())
}
pub async fn remove_current_connection(&self) -> Result<Option<Connection>, AppStateError> {
let persistence = self.persistence().await;
Ok(persistence.take(PersistenceKey::Connection).await?)
}
pub async fn get_connection_name(&self) -> Result<Option<String>, AppStateError> {
let persistence = self.persistence().await;
Ok(persistence.get(PersistenceKey::ConnectionName).await?)
}
pub async fn update_connection_name(
&self,
name: impl Into<String>,
) -> Result<(), AppStateError> {
let persistence = self.persistence().await;
let name = name.into();
persistence
.set(PersistenceKey::ConnectionName, &name)
.await?;
Ok(())
}
pub async fn get_or_init_connection_id(&self) -> Result<String, AppStateError> {
const KEY: PersistenceKey = PersistenceKey::ConnectionId;
let persistence = self.persistence().await;
Ok(if let Some(connection_id) = persistence.get(KEY).await? {
connection_id
} else {
let connection_id = nanoid::nanoid!();
persistence.set(KEY, &connection_id).await?;
connection_id
})
}
pub async fn add_connection(
&self,
connection: impl Into<Connection>,
) -> Result<Vec<Connection>, AppStateError> {
let persistence = self.persistence().await;
let connection = connection.into();
let mut connections: Vec<Connection> = persistence
.get(PersistenceKey::Connections)
.await?
.unwrap_or_default();
if self.get_current_connection().await?.is_none() {
self.set_current_connection(connection.clone()).await?;
}
connections.push(connection);
persistence
.set(PersistenceKey::Connections, &connections)
.await?;
Ok(connections)
}
pub async fn delete_connection(&self, name: &str) -> Result<Vec<Connection>, AppStateError> {
let persistence = self.persistence().await;
let mut connections: Vec<Connection> = persistence
.get(PersistenceKey::Connections)
.await?
.unwrap_or_default();
if let Some(current_connection) = self.get_current_connection().await?
&& current_connection.name == name
{
self.remove_current_connection().await?;
}
connections.retain(|x| x.name != name);
persistence
.set(PersistenceKey::Connections, &connections)
.await?;
Ok(connections)
}
pub async fn update_connection(
&self,
name: &str,
connection: impl Into<Connection>,
) -> Result<Vec<Connection>, AppStateError> {
let connection = connection.into();
let persistence = self.persistence().await;
let mut connections: Vec<Connection> = persistence
.get(PersistenceKey::Connections)
.await?
.unwrap_or_default();
if let Some(current_connection) = self.get_current_connection().await?
&& current_connection.name == name
{
self.set_current_connection(connection.clone()).await?;
}
for existing in &mut connections {
if existing.name == name {
*existing = connection;
persistence
.set(PersistenceKey::Connections, &connections)
.await?;
break;
}
}
Ok(connections)
}
pub(crate) async fn persist_default_download_location(
&self,
path: impl AsRef<str>,
) -> Result<(), AppStateError> {
let path = path.as_ref();
let persistence = self.persistence().await;
persistence
.set(PersistenceKey::DefaultDownloadLocation, &path.to_string())
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test_log::test(switchy_async::test)]
async fn test_app_state_with_persistence_in_memory() {
let state = AppState::new()
.with_persistence_in_memory()
.await
.expect("Failed to create in-memory persistence");
assert!(state.persistence.read().await.is_some());
}
#[test_log::test(switchy_async::test)]
async fn test_app_state_add_and_get_connections() {
let state = AppState::new()
.with_persistence_in_memory()
.await
.expect("Failed to create in-memory persistence");
let connection = Connection {
name: "Test Server".to_string(),
api_url: "https://test.example.com".to_string(),
};
let connections = state
.add_connection(connection.clone())
.await
.expect("Failed to add connection");
assert_eq!(connections.len(), 1);
assert_eq!(connections[0].name, "Test Server");
let retrieved_connections = state
.get_connections()
.await
.expect("Failed to get connections");
assert_eq!(retrieved_connections.len(), 1);
assert_eq!(retrieved_connections[0], connection);
}
#[test_log::test(switchy_async::test)]
async fn test_app_state_set_and_get_current_connection() {
let state = AppState::new()
.with_persistence_in_memory()
.await
.expect("Failed to create in-memory persistence");
let connection = Connection {
name: "Current Server".to_string(),
api_url: "https://current.example.com".to_string(),
};
state
.set_current_connection(&connection)
.await
.expect("Failed to set current connection");
let current = state
.get_current_connection()
.await
.expect("Failed to get current connection")
.expect("No current connection found");
assert_eq!(current, connection);
}
#[test_log::test(switchy_async::test)]
async fn test_app_state_remove_current_connection() {
let state = AppState::new()
.with_persistence_in_memory()
.await
.expect("Failed to create in-memory persistence");
let connection = Connection {
name: "Temp Server".to_string(),
api_url: "https://temp.example.com".to_string(),
};
state
.set_current_connection(&connection)
.await
.expect("Failed to set current connection");
let removed = state
.remove_current_connection()
.await
.expect("Failed to remove current connection")
.expect("No connection to remove");
assert_eq!(removed, connection);
let current = state
.get_current_connection()
.await
.expect("Failed to get current connection");
assert!(current.is_none());
}
#[test_log::test(switchy_async::test)]
async fn test_app_state_delete_connection() {
let state = AppState::new()
.with_persistence_in_memory()
.await
.expect("Failed to create in-memory persistence");
let connection1 = Connection {
name: "Server 1".to_string(),
api_url: "https://server1.example.com".to_string(),
};
let connection2 = Connection {
name: "Server 2".to_string(),
api_url: "https://server2.example.com".to_string(),
};
state
.add_connection(connection1.clone())
.await
.expect("Failed to add connection 1");
state
.add_connection(connection2.clone())
.await
.expect("Failed to add connection 2");
let remaining = state
.delete_connection("Server 1")
.await
.expect("Failed to delete connection");
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].name, "Server 2");
}
#[test_log::test(switchy_async::test)]
async fn test_app_state_delete_current_connection() {
let state = AppState::new()
.with_persistence_in_memory()
.await
.expect("Failed to create in-memory persistence");
let connection = Connection {
name: "Current".to_string(),
api_url: "https://current.example.com".to_string(),
};
state
.add_connection(connection.clone())
.await
.expect("Failed to add connection");
let current = state
.get_current_connection()
.await
.expect("Failed to get current connection");
assert!(current.is_some());
state
.delete_connection("Current")
.await
.expect("Failed to delete connection");
let current_after = state
.get_current_connection()
.await
.expect("Failed to get current connection");
assert!(current_after.is_none());
}
#[test_log::test(switchy_async::test)]
async fn test_app_state_update_connection() {
let state = AppState::new()
.with_persistence_in_memory()
.await
.expect("Failed to create in-memory persistence");
let connection = Connection {
name: "Original".to_string(),
api_url: "https://original.example.com".to_string(),
};
state
.add_connection(connection.clone())
.await
.expect("Failed to add connection");
let updated_connection = Connection {
name: "Updated".to_string(),
api_url: "https://updated.example.com".to_string(),
};
let connections = state
.update_connection("Original", updated_connection.clone())
.await
.expect("Failed to update connection");
assert_eq!(connections.len(), 1);
assert_eq!(connections[0].name, "Updated");
assert_eq!(connections[0].api_url, "https://updated.example.com");
}
#[test_log::test(switchy_async::test)]
async fn test_app_state_get_or_init_connection_id() {
let state = AppState::new()
.with_persistence_in_memory()
.await
.expect("Failed to create in-memory persistence");
let connection_id1 = state
.get_or_init_connection_id()
.await
.expect("Failed to get connection ID");
assert!(!connection_id1.is_empty());
let connection_id2 = state
.get_or_init_connection_id()
.await
.expect("Failed to get connection ID");
assert_eq!(connection_id1, connection_id2);
}
#[test_log::test(switchy_async::test)]
async fn test_app_state_connection_name_persistence() {
let state = AppState::new()
.with_persistence_in_memory()
.await
.expect("Failed to create in-memory persistence");
state
.update_connection_name("My Connection")
.await
.expect("Failed to update connection name");
let name = state
.get_connection_name()
.await
.expect("Failed to get connection name")
.expect("No connection name found");
assert_eq!(name, "My Connection");
}
#[test_log::test(switchy_async::test)]
async fn test_app_state_default_download_location() {
let state = AppState::new()
.with_persistence_in_memory()
.await
.expect("Failed to create in-memory persistence");
let path = "/downloads/music";
state
.set_default_download_location(path.to_string())
.await
.expect("Failed to set default download location");
let retrieved_path = state.get_default_download_location();
assert_eq!(retrieved_path, Some(path.to_string()));
}
}