mod app_bundle;
mod app_manifest;
mod dna_gamut;
pub mod error;
use crate::dna::DnaBundle;
pub use app_bundle::*;
pub use app_manifest::app_manifest_validated::*;
pub use app_manifest::*;
use derive_more::Into;
pub use dna_gamut::*;
use holo_hash::{AgentPubKey, DnaHash};
use holochain_serialized_bytes::prelude::*;
use holochain_util::ffs;
use holochain_zome_types::prelude::*;
use itertools::Itertools;
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
};
use self::error::{AppError, AppResult};
pub type InstalledAppId = String;
pub type AppRoleId = String;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DnaSource {
Path(PathBuf),
Bundle(DnaBundle),
Hash(DnaHash),
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct RegisterDnaPayload {
pub uid: Option<String>,
pub properties: Option<YamlProperties>,
#[serde(flatten)]
pub source: DnaSource,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct CreateCloneCellPayload {
pub properties: Option<YamlProperties>,
pub dna_hash: DnaHash,
pub agent_key: AgentPubKey,
pub installed_app_id: InstalledAppId,
pub role_id: AppRoleId,
pub membrane_proof: Option<MembraneProof>,
}
impl CreateCloneCellPayload {
pub fn cell_id(&self) -> CellId {
CellId::new(self.dna_hash.clone(), self.agent_key.clone())
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct InstallAppPayload {
pub installed_app_id: InstalledAppId,
pub agent_key: AgentPubKey,
pub dnas: Vec<InstallAppDnaPayload>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct InstallAppBundlePayload {
#[serde(flatten)]
pub source: AppBundleSource,
pub agent_key: AgentPubKey,
pub installed_app_id: Option<InstalledAppId>,
pub membrane_proofs: HashMap<AppRoleId, MembraneProof>,
pub uid: Option<Uid>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AppBundleSource {
Bundle(AppBundle),
Path(PathBuf),
}
impl AppBundleSource {
pub async fn resolve(self) -> Result<AppBundle, AppBundleError> {
Ok(match self {
Self::Bundle(bundle) => bundle,
Self::Path(path) => AppBundle::decode(&ffs::read(&path).await?)?,
})
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct InstallAppDnaPayload {
pub hash: DnaHash,
pub role_id: AppRoleId,
pub membrane_proof: Option<MembraneProof>,
}
impl InstallAppDnaPayload {
pub fn hash_only(hash: DnaHash, role_id: AppRoleId) -> Self {
Self {
hash,
role_id,
membrane_proof: None,
}
}
}
#[deprecated = "can be removed after the old way of installing apps (`InstallApp`) is phased out"]
#[derive(Clone, Debug, Into, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct InstalledCell {
cell_id: CellId,
role_id: AppRoleId,
}
impl InstalledCell {
pub fn new(cell_id: CellId, role_id: AppRoleId) -> Self {
Self { cell_id, role_id }
}
pub fn into_id(self) -> CellId {
self.cell_id
}
pub fn into_role_id(self) -> AppRoleId {
self.role_id
}
pub fn into_inner(self) -> (CellId, AppRoleId) {
(self.cell_id, self.role_id)
}
pub fn as_id(&self) -> &CellId {
&self.cell_id
}
pub fn as_role_id(&self) -> &AppRoleId {
&self.role_id
}
}
#[derive(
Clone,
Debug,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
derive_more::Constructor,
shrinkwraprs::Shrinkwrap,
)]
#[shrinkwrap(mutable, unsafe_ignore_visibility)]
pub struct InstalledApp {
#[shrinkwrap(main_field)]
app: InstalledAppCommon,
pub status: AppStatus,
}
impl InstalledApp {
pub fn new_fresh(app: InstalledAppCommon) -> Self {
Self {
app,
status: AppStatus::Disabled(DisabledAppReason::NeverStarted),
}
}
#[cfg(feature = "test_utils")]
pub fn new_running(app: InstalledAppCommon) -> Self {
Self {
app,
status: AppStatus::Running,
}
}
pub fn into_app_and_status(self) -> (InstalledAppCommon, AppStatus) {
(self.app, self.status)
}
pub fn status(&self) -> &AppStatus {
&self.status
}
pub fn id(&self) -> &InstalledAppId {
&self.app.installed_app_id
}
}
impl automap::AutoMapped for InstalledApp {
type Key = InstalledAppId;
fn key(&self) -> &Self::Key {
&self.app.installed_app_id
}
}
pub type InstalledAppMap = automap::AutoHashMap<InstalledApp>;
#[derive(
Clone,
Debug,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
derive_more::From,
shrinkwraprs::Shrinkwrap,
)]
#[shrinkwrap(mutable, unsafe_ignore_visibility)]
pub struct RunningApp(InstalledAppCommon);
impl RunningApp {
pub fn into_stopped(self, reason: StoppedAppReason) -> StoppedApp {
StoppedApp {
app: self.0,
reason,
}
}
pub fn into_common(self) -> InstalledAppCommon {
self.0
}
}
impl From<RunningApp> for InstalledApp {
fn from(app: RunningApp) -> Self {
Self {
app: app.into_common(),
status: AppStatus::Running,
}
}
}
#[derive(
Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, shrinkwraprs::Shrinkwrap,
)]
#[shrinkwrap(mutable, unsafe_ignore_visibility)]
pub struct StoppedApp {
#[shrinkwrap(main_field)]
app: InstalledAppCommon,
reason: StoppedAppReason,
}
impl StoppedApp {
#[deprecated = "should only be constructable through conversions from other types"]
pub fn new(app: InstalledAppCommon, reason: StoppedAppReason) -> Self {
Self { app, reason }
}
pub fn new_fresh(app: InstalledAppCommon) -> Self {
Self {
app,
reason: StoppedAppReason::Disabled(DisabledAppReason::NeverStarted),
}
}
pub fn from_app(app: &InstalledApp) -> Option<Self> {
StoppedAppReason::from_status(app.status()).map(|reason| Self {
app: app.as_ref().clone(),
reason,
})
}
pub fn into_active(self) -> RunningApp {
RunningApp(self.app)
}
pub fn into_common(self) -> InstalledAppCommon {
self.app
}
}
impl From<StoppedApp> for InstalledAppCommon {
fn from(d: StoppedApp) -> Self {
d.app
}
}
impl From<StoppedApp> for InstalledApp {
fn from(d: StoppedApp) -> Self {
Self {
app: d.app,
status: d.reason.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct InstalledAppCommon {
installed_app_id: InstalledAppId,
_agent_key: AgentPubKey,
role_assignments: HashMap<AppRoleId, AppRoleAssignment>,
}
impl InstalledAppCommon {
pub fn new<S: ToString, I: IntoIterator<Item = (AppRoleId, AppRoleAssignment)>>(
installed_app_id: S,
_agent_key: AgentPubKey,
role_assignments: I,
) -> Self {
InstalledAppCommon {
installed_app_id: installed_app_id.to_string(),
_agent_key,
role_assignments: role_assignments.into_iter().collect(),
}
}
pub fn id(&self) -> &InstalledAppId {
&self.installed_app_id
}
pub fn provisioned_cells(&self) -> impl Iterator<Item = (&AppRoleId, &CellId)> {
self.role_assignments
.iter()
.filter_map(|(role_id, role)| role.provisioned_cell().map(|c| (role_id, c)))
}
pub fn into_provisioned_cells(self) -> impl Iterator<Item = (AppRoleId, CellId)> {
self.role_assignments
.into_iter()
.filter_map(|(role_id, role)| role.into_provisioned_cell().map(|c| (role_id, c)))
}
pub fn cloned_cells(&self) -> impl Iterator<Item = &CellId> {
self.role_assignments
.iter()
.map(|(_, role)| &role.clones)
.flatten()
}
pub fn all_cells(&self) -> impl Iterator<Item = &CellId> {
self.provisioned_cells()
.map(|(_, c)| c)
.chain(self.cloned_cells())
}
pub fn required_cells(&self) -> impl Iterator<Item = &CellId> {
self.all_cells()
}
pub fn role(&self, role_id: &AppRoleId) -> AppResult<&AppRoleAssignment> {
self.role_assignments
.get(role_id)
.ok_or_else(|| AppError::AppRoleIdMissing(role_id.clone()))
}
fn role_mut(&mut self, role_id: &AppRoleId) -> AppResult<&mut AppRoleAssignment> {
self.role_assignments
.get_mut(role_id)
.ok_or_else(|| AppError::AppRoleIdMissing(role_id.clone()))
}
pub fn roles(&self) -> &HashMap<AppRoleId, AppRoleAssignment> {
&self.role_assignments
}
pub fn add_clone(&mut self, role_id: &AppRoleId, cell_id: CellId) -> AppResult<()> {
let role = self.role_mut(role_id)?;
assert_eq!(
cell_id.agent_pubkey(),
role.agent_key(),
"A clone cell must use the same agent key as the role it is added to"
);
if role.clones.len() as u32 >= role.clone_limit {
return Err(AppError::CloneLimitExceeded(role.clone_limit, role.clone()));
}
let _ = role.clones.insert(cell_id);
Ok(())
}
pub fn remove_clone(&mut self, role_id: &AppRoleId, cell_id: &CellId) -> AppResult<bool> {
let role = self.role_mut(role_id)?;
Ok(role.clones.remove(cell_id))
}
pub fn _agent_key(&self) -> &AgentPubKey {
&self._agent_key
}
pub fn new_legacy<S: ToString, I: IntoIterator<Item = InstalledCell>>(
installed_app_id: S,
installed_cells: I,
) -> AppResult<Self> {
let installed_app_id = installed_app_id.to_string();
let installed_cells: Vec<_> = installed_cells.into_iter().collect();
let _agent_key = installed_cells
.get(0)
.expect("Can't create app with 0 cells")
.cell_id
.agent_pubkey()
.to_owned();
if installed_cells
.iter()
.any(|c| *c.cell_id.agent_pubkey() != _agent_key)
{
tracing::warn!(
"It's kind of an informal convention that all cells in a legacy installation should use the same agent key. But, no big deal... Cell data: {:#?}",
installed_cells
);
}
let duplicates: Vec<AppRoleId> = installed_cells
.iter()
.map(|c| c.role_id.to_owned())
.counts()
.into_iter()
.filter_map(|(role_id, count)| if count > 1 { Some(role_id) } else { None })
.collect();
if !duplicates.is_empty() {
return Err(AppError::DuplicateAppRoleIds(installed_app_id, duplicates));
}
let roles = installed_cells
.into_iter()
.map(|InstalledCell { role_id, cell_id }| {
let role = AppRoleAssignment {
base_cell_id: cell_id,
is_provisioned: true,
clones: HashSet::new(),
clone_limit: 0,
};
(role_id, role)
})
.collect();
Ok(Self {
installed_app_id,
_agent_key,
role_assignments: roles,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
#[serde(rename_all = "snake_case")]
pub enum AppStatus {
Running,
Paused(PausedAppReason),
Disabled(DisabledAppReason),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum AppStatusKind {
Running,
Paused,
Disabled,
}
impl From<AppStatus> for AppStatusKind {
fn from(status: AppStatus) -> Self {
match status {
AppStatus::Running => Self::Running,
AppStatus::Paused(_) => Self::Paused,
AppStatus::Disabled(_) => Self::Disabled,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AppStatusTransition {
Start,
Pause(PausedAppReason),
Enable,
Disable(DisabledAppReason),
}
impl AppStatus {
pub fn is_enabled(&self) -> bool {
matches!(self, Self::Running | Self::Paused(_))
}
pub fn is_running(&self) -> bool {
matches!(self, Self::Running)
}
pub fn is_paused(&self) -> bool {
matches!(self, Self::Paused(_))
}
pub fn transition(&mut self, transition: AppStatusTransition) -> AppStatusFx {
use AppStatus::*;
use AppStatusFx::*;
use AppStatusTransition::*;
match (&self, transition) {
(Running, Pause(reason)) => Some((Paused(reason), SpinDown)),
(Running, Disable(reason)) => Some((Disabled(reason), SpinDown)),
(Running, Start) | (Running, Enable) => None,
(Paused(_), Start) => Some((Running, SpinUp)),
(Paused(_), Enable) => Some((Running, SpinUp)),
(Paused(_), Disable(reason)) => Some((Disabled(reason), SpinDown)),
(Paused(_), Pause(_)) => None,
(Disabled(_), Enable) => Some((Running, SpinUp)),
(Disabled(_), Pause(_)) | (Disabled(_), Disable(_)) | (Disabled(_), Start) => None,
}
.map(|(new_status, delta)| {
*self = new_status;
delta
})
.unwrap_or(NoChange)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[must_use = "be sure to run this value through `process_app_status_fx` to handle any transition effects"]
pub enum AppStatusFx {
NoChange,
SpinDown,
SpinUp,
Both,
}
impl Default for AppStatusFx {
fn default() -> Self {
Self::NoChange
}
}
impl AppStatusFx {
pub fn combine(self, other: Self) -> Self {
use AppStatusFx::*;
match (self, other) {
(NoChange, a) | (a, NoChange) => a,
(SpinDown, SpinDown) => SpinDown,
(SpinUp, SpinUp) => SpinUp,
(Both, _) | (_, Both) => Both,
(SpinDown, SpinUp) | (SpinUp, SpinDown) => Both,
}
}
}
#[derive(
Clone,
Debug,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
SerializedBytes,
derive_more::From,
)]
#[serde(rename_all = "snake_case")]
pub enum StoppedAppReason {
Paused(PausedAppReason),
Disabled(DisabledAppReason),
}
impl StoppedAppReason {
pub fn from_status(status: &AppStatus) -> Option<Self> {
match status {
AppStatus::Paused(reason) => Some(Self::Paused(reason.clone())),
AppStatus::Disabled(reason) => Some(Self::Disabled(reason.clone())),
AppStatus::Running => None,
}
}
}
impl From<StoppedAppReason> for AppStatus {
fn from(reason: StoppedAppReason) -> Self {
match reason {
StoppedAppReason::Paused(reason) => Self::Paused(reason),
StoppedAppReason::Disabled(reason) => Self::Disabled(reason),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
#[serde(rename_all = "snake_case")]
pub enum PausedAppReason {
Error(String),
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
#[serde(rename_all = "snake_case")]
pub enum DisabledAppReason {
NeverStarted,
User,
Error(String),
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AppRoleAssignment {
base_cell_id: CellId,
is_provisioned: bool,
clone_limit: u32,
clones: HashSet<CellId>,
}
impl AppRoleAssignment {
pub fn new(base_cell_id: CellId, is_provisioned: bool, clone_limit: u32) -> Self {
Self {
base_cell_id,
is_provisioned,
clone_limit,
clones: HashSet::new(),
}
}
pub fn cell_id(&self) -> &CellId {
&self.base_cell_id
}
pub fn dna_hash(&self) -> &DnaHash {
self.base_cell_id.dna_hash()
}
pub fn agent_key(&self) -> &AgentPubKey {
self.base_cell_id.agent_pubkey()
}
pub fn provisioned_cell(&self) -> Option<&CellId> {
if self.is_provisioned {
Some(&self.base_cell_id)
} else {
None
}
}
pub fn into_provisioned_cell(self) -> Option<CellId> {
if self.is_provisioned {
Some(self.base_cell_id)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::{AppRoleAssignment, RunningApp};
use crate::prelude::*;
use ::fixt::prelude::*;
use std::collections::HashSet;
#[test]
fn clone_management() {
let base_cell_id = fixt!(CellId);
let agent = base_cell_id.agent_pubkey().clone();
let new_clone = || CellId::new(fixt!(DnaHash), agent.clone());
let role1 = AppRoleAssignment::new(base_cell_id, false, 3);
let agent = fixt!(AgentPubKey);
let role_id: AppRoleId = "role_id".into();
let mut app: RunningApp =
InstalledAppCommon::new("app", agent.clone(), vec![(role_id.clone(), role1)]).into();
let clones: Vec<_> = vec![new_clone(), new_clone(), new_clone()];
app.add_clone(&role_id, clones[0].clone()).unwrap();
app.add_clone(&role_id, clones[1].clone()).unwrap();
app.add_clone(&role_id, clones[2].clone()).unwrap();
matches::assert_matches!(
app.add_clone(&role_id, new_clone()),
Err(AppError::CloneLimitExceeded(3, _))
);
assert_eq!(
app.cloned_cells().collect::<HashSet<_>>(),
maplit::hashset! { &clones[0], &clones[1], &clones[2] }
);
assert_eq!(app.remove_clone(&role_id, &clones[1]).unwrap(), true);
assert_eq!(app.remove_clone(&role_id, &clones[1]).unwrap(), false);
assert_eq!(
app.cloned_cells().collect::<HashSet<_>>(),
maplit::hashset! { &clones[0], &clones[2] }
);
app.add_clone(&role_id, clones[0].clone()).unwrap();
assert_eq!(app.cloned_cells().count(), 2);
assert_eq!(
app.cloned_cells().collect::<HashSet<_>>(),
app.all_cells().collect::<HashSet<_>>()
);
}
}