use super::error::{ConductorError, ConductorResult};
use holochain_conductor_api::config::InterfaceDriver;
use holochain_conductor_api::signal_subscription::SignalSubscription;
use holochain_p2p::NetworkCompatParams;
use holochain_types::prelude::*;
use holochain_types::websocket::AllowedOrigins;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
#[derive(Clone, Deserialize, Serialize, Debug, SerializedBytes)]
#[cfg_attr(test, derive(PartialEq))]
#[serde(transparent)]
pub struct ConductorStateTag(pub Arc<str>);
impl Default for ConductorStateTag {
fn default() -> Self {
Self(nanoid::nanoid!().into())
}
}
#[serde_with::serde_as]
#[derive(Clone, Deserialize, Serialize, Default, Debug, SerializedBytes)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ConductorState {
#[serde(default)]
tag: ConductorStateTag,
#[serde(default)]
installed_apps: InstalledAppMap,
#[serde_as(as = "Vec<(_, _)>")]
#[serde(default)]
pub(crate) app_interfaces: HashMap<AppInterfaceId, AppInterfaceConfig>,
}
#[derive(Clone, Deserialize, Serialize, Debug, Hash, PartialEq, Eq)]
pub struct AppInterfaceId {
port: u16,
id: Option<String>,
}
impl Default for AppInterfaceId {
fn default() -> Self {
Self::new(0)
}
}
impl AppInterfaceId {
pub fn new(port: u16) -> Self {
let id = if port == 0 {
Some(nanoid::nanoid!())
} else {
None
};
Self { port, id }
}
pub(crate) fn from_parts(port: u16, id: Option<String>) -> Self {
Self { port, id }
}
pub fn port(&self) -> u16 {
self.port
}
pub(crate) fn id(&self) -> &Option<String> {
&self.id
}
}
impl ConductorState {
pub fn tag(&self) -> &ConductorStateTag {
&self.tag
}
pub(crate) fn from_parts(
tag: ConductorStateTag,
installed_apps: InstalledAppMap,
app_interfaces: HashMap<AppInterfaceId, AppInterfaceConfig>,
) -> Self {
Self {
tag,
installed_apps,
app_interfaces,
}
}
#[cfg(test)]
pub fn set_tag(&mut self, tag: ConductorStateTag) {
self.tag = tag;
}
pub fn installed_apps(&self) -> &InstalledAppMap {
&self.installed_apps
}
#[deprecated = "Bare mutable access isn't the best idea"]
pub fn installed_apps_mut(&mut self) -> &mut InstalledAppMap {
&mut self.installed_apps
}
pub fn enabled_apps(&self) -> impl Iterator<Item = (&InstalledAppId, &InstalledApp)> + '_ {
self.installed_apps
.iter()
.filter(|(_, app)| app.status == AppStatus::Enabled)
}
pub fn disabled_apps(&self) -> impl Iterator<Item = (&InstalledAppId, &InstalledApp)> + '_ {
self.installed_apps
.iter()
.filter(|(_, app)| matches!(app.status, AppStatus::Disabled(_)))
}
pub fn get_app(&self, id: &InstalledAppId) -> ConductorResult<&InstalledApp> {
self.installed_apps
.get(id)
.ok_or_else(|| ConductorError::AppNotInstalled(id.clone()))
}
pub fn get_app_mut(&mut self, id: &InstalledAppId) -> ConductorResult<&mut InstalledApp> {
self.installed_apps
.get_mut(id)
.ok_or_else(|| ConductorError::AppNotInstalled(id.clone()))
}
pub fn remove_app(&mut self, id: &InstalledAppId) -> ConductorResult<InstalledApp> {
self.installed_apps
.swap_remove(id)
.ok_or_else(|| ConductorError::AppNotInstalled(id.clone()))
}
pub fn add_app(&mut self, app: InstalledAppCommon) -> ConductorResult<InstalledApp> {
if self.installed_apps.contains_key(app.id()) {
return Err(ConductorError::AppAlreadyInstalled(app.id().clone()));
}
let app = InstalledApp::new(app, AppStatus::Disabled(DisabledAppReason::NeverStarted));
self.installed_apps.insert(app.id().clone(), app.clone());
Ok(app)
}
pub fn add_app_awaiting_memproofs(
&mut self,
app: InstalledAppCommon,
) -> ConductorResult<InstalledApp> {
if self.installed_apps.contains_key(app.id()) {
return Err(ConductorError::AppAlreadyInstalled(app.id().clone()));
}
let app = InstalledApp::new(app, AppStatus::AwaitingMemproofs);
self.installed_apps.insert(app.id().clone(), app.clone());
Ok(app)
}
pub fn interface_by_id(&self, id: &AppInterfaceId) -> Option<AppInterfaceConfig> {
self.app_interfaces.get(id).cloned()
}
pub fn find_app_containing_cell(&self, cell_id: &CellId) -> Option<&InstalledApp> {
self.installed_apps
.values()
.find(|app| app.all_cells().any(|id| id == *cell_id))
}
pub fn get_network_compat(&self) -> NetworkCompatParams {
tracing::warn!("Using default NetworkCompatParams");
Default::default()
}
pub fn get_dependent_apps(
&self,
id: &InstalledAppId,
protected_only: bool,
) -> ConductorResult<Vec<InstalledAppId>> {
let app = self.get_app(id)?;
let cell_ids: HashSet<_> = app.all_cells().collect();
Ok(self
.installed_apps
.iter()
.filter(|(_, app)| {
app.role_assignments.values().any(|r| match r {
AppRoleAssignment::Primary(_) => false,
AppRoleAssignment::Dependency(d) => {
cell_ids.contains(&d.cell_id) && (!protected_only || d.protected)
}
})
})
.map(|(id, _)| id.clone())
.collect())
}
}
#[derive(Clone, Deserialize, Serialize, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct AppInterfaceConfig {
pub signal_subscriptions: HashMap<InstalledAppId, SignalSubscription>,
pub installed_app_id: Option<InstalledAppId>,
pub driver: InterfaceDriver,
}
impl AppInterfaceConfig {
pub fn websocket(
port: u16,
danger_bind_addr: Option<String>,
allowed_origins: AllowedOrigins,
installed_app_id: Option<InstalledAppId>,
) -> Self {
Self {
signal_subscriptions: HashMap::new(),
installed_app_id,
driver: InterfaceDriver::Websocket {
port,
danger_bind_addr,
allowed_origins,
},
}
}
}
#[cfg(test)]
mod tests {
use super::ConductorState;
use ::fixt::fixt;
use hdk::prelude::CellId;
use holo_hash::fixt::{AgentPubKeyFixturator, DnaHashFixturator};
use holochain_timestamp::Timestamp;
use holochain_types::app::{
AppManifestV0Builder, AppRoleAssignment, AppRolePrimary, AppStatus, DisabledAppReason,
InstalledApp, InstalledAppCommon,
};
#[test]
fn app_status() {
let mut state = ConductorState::default();
let agent = fixt!(AgentPubKey);
let dna_hash = fixt!(DnaHash);
let cell_id = CellId::new(dna_hash.clone(), agent.clone());
assert_eq!(state.enabled_apps().count(), 0);
assert_eq!(state.disabled_apps().count(), 0);
assert_eq!(state.installed_apps().len(), 0);
assert!(state.find_app_containing_cell(&cell_id).is_none());
let app_manifest = AppManifestV0Builder::default()
.name("name".to_string())
.description(None)
.roles(vec![])
.build()
.unwrap();
let app_id = "app_id";
let app = InstalledAppCommon::new(
app_id,
agent.clone(),
[(
"role_1".to_string(),
AppRoleAssignment::Primary(AppRolePrimary::new(dna_hash, true, 0)),
)],
app_manifest.into(),
Timestamp::now(),
)
.unwrap();
state.add_app(app.clone()).unwrap();
assert_eq!(state.enabled_apps().count(), 0);
assert_eq!(state.disabled_apps().count(), 1);
assert_eq!(state.installed_apps().len(), 1);
let installed_app = InstalledApp::new(
app.clone(),
AppStatus::Disabled(DisabledAppReason::NeverStarted),
);
assert_eq!(
state.installed_apps().first().unwrap(),
(&app_id.to_string(), &installed_app)
);
assert_eq!(
state.find_app_containing_cell(&cell_id).unwrap(),
&installed_app
);
state.get_app_mut(&app_id.to_string()).unwrap().status = AppStatus::Enabled;
assert_eq!(state.enabled_apps().count(), 1);
assert_eq!(state.disabled_apps().count(), 0);
assert_eq!(state.installed_apps().len(), 1);
let installed_app = InstalledApp::new(app.clone(), AppStatus::Enabled);
assert_eq!(
state.installed_apps().first().unwrap(),
(&app_id.to_string(), &installed_app)
);
assert_eq!(
state.find_app_containing_cell(&cell_id).unwrap(),
&installed_app
);
}
}