#![allow(deprecated)]
mod app_bundle;
mod app_manifest;
mod error;
use crate::{dna::DnaBundle, prelude::*};
pub use app_bundle::*;
pub use app_manifest::app_manifest_validated::*;
pub use app_manifest::*;
use bytes::Buf;
use derive_more::Into;
pub use error::*;
use holo_hash::{AgentPubKey, DnaHash};
use holochain_serialized_bytes::prelude::*;
use holochain_util::ffs;
use holochain_zome_types::cell::CloneId;
use holochain_zome_types::prelude::*;
use indexmap::IndexMap;
use itertools::Itertools;
use std::{collections::HashMap, path::PathBuf};
pub type InstalledAppId = String;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub enum DnaSource {
Path(PathBuf),
Bundle(Box<DnaBundle>),
Hash(DnaHash),
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub enum CoordinatorSource {
Path(PathBuf),
Bundle(Box<CoordinatorBundle>),
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub struct UpdateCoordinatorsPayload {
pub cell_id: CellId,
pub source: CoordinatorSource,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/app/types.ts"))]
pub struct CreateCloneCellPayload {
pub role_name: RoleName,
pub modifiers: DnaModifiersOpt<YamlProperties>,
#[cfg_attr(feature = "ts_rs", ts(as = "Option<MembraneProofTs>"))]
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
pub membrane_proof: Option<MembraneProof>,
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
pub name: Option<String>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/app/types.ts"))]
pub struct DisableCloneCellPayload {
pub clone_cell_id: CloneCellId,
}
pub type EnableCloneCellPayload = DisableCloneCellPayload;
#[cfg(feature = "ts_rs")]
holo_hash::ts_alias!(
EnableCloneCellPayloadTs,
"EnableCloneCellPayload",
"DisableCloneCellPayload",
"api/app/types.ts",
deps: [DisableCloneCellPayload]
);
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub struct DeleteCloneCellPayload {
pub app_id: InstalledAppId,
pub clone_cell_id: CloneCellId,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub struct InstallAppPayload {
pub source: AppBundleSource,
#[serde(default)]
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
pub agent_key: Option<AgentPubKey>,
#[serde(default)]
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
pub installed_app_id: Option<InstalledAppId>,
#[serde(default)]
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
#[cfg_attr(feature = "ts_rs", ts(as = "Option<NetworkSeedTs>"))]
pub network_seed: Option<NetworkSeed>,
#[serde(default)]
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
#[cfg_attr(feature = "ts_rs", ts(as = "Option<RoleSettingsMapTs>"))]
pub roles_settings: Option<RoleSettingsMap>,
#[serde(default)]
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
pub ignore_genesis_failure: bool,
#[serde(default)]
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
pub restore_from_dht: bool,
}
pub type MemproofMap = HashMap<RoleName, MembraneProof>;
#[cfg(feature = "ts_rs")]
holo_hash::ts_alias!(
MemproofMapTs,
"MemproofMap",
"Record<string, MembraneProof>",
"api/admin/types.ts",
deps: [MembraneProofTs]
);
pub type ModifiersMap = HashMap<RoleName, DnaModifiersOpt<YamlProperties>>;
pub type ExistingCellsMap = HashMap<RoleName, CellId>;
pub type InitPropertiesMap = HashMap<RoleName, InitProperties>;
pub type RoleSettingsMap = HashMap<RoleName, RoleSettings>;
#[cfg(feature = "ts_rs")]
holo_hash::ts_alias!(
RoleSettingsMapTs,
"RoleSettingsMap",
"Record<string, RoleSettings>",
"api/admin/types.ts",
deps: [RoleSettings]
);
pub type RoleSettingsMapYaml = HashMap<RoleName, RoleSettingsYaml>;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub enum RoleSettings {
#[deprecated(
since = "0.6.0-dev.17",
note = "For late binding, update the coordinators of a DNA. For calling cells of other apps, use bridge calls."
)]
UseExisting {
cell_id: CellId,
},
Provisioned {
#[cfg_attr(feature = "ts_rs", ts(as = "Option<MembraneProofTs>"))]
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
membrane_proof: Option<MembraneProof>,
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
modifiers: Option<DnaModifiersOpt<YamlProperties>>,
#[cfg_attr(feature = "ts_rs", ts(optional = nullable))]
init_properties: Option<InitProperties>,
},
}
impl Default for RoleSettings {
fn default() -> Self {
Self::Provisioned {
membrane_proof: None,
modifiers: None,
init_properties: None,
}
}
}
impl From<RoleSettingsYaml> for RoleSettings {
fn from(role_settings: RoleSettingsYaml) -> Self {
match role_settings {
RoleSettingsYaml::Provisioned {
membrane_proof,
modifiers,
init_properties,
} => Self::Provisioned {
membrane_proof,
modifiers,
init_properties,
},
#[allow(deprecated)]
RoleSettingsYaml::UseExisting { cell_id } => Self::UseExisting { cell_id },
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RoleSettingsYaml {
#[deprecated(
since = "0.6.0-dev.17",
note = "For late binding, update the coordinators of a DNA. For calling cells of other apps, use bridge calls."
)]
UseExisting {
cell_id: CellId,
},
Provisioned {
membrane_proof: Option<MembraneProof>,
modifiers: Option<DnaModifiersOpt<YamlProperties>>,
init_properties: Option<InitProperties>,
},
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub enum AppBundleSource {
Bytes(#[cfg_attr(feature = "ts_rs", ts(type = "Uint8Array"))] bytes::Bytes),
Path(PathBuf),
}
impl AppBundleSource {
pub async fn resolve(self) -> Result<AppBundle, AppBundleError> {
Ok(match self {
Self::Bytes(bytes) => AppBundle::unpack(bytes.reader())?,
Self::Path(path) => {
let content = ffs::read(&path).await?;
AppBundle::unpack(content.as_slice())?
}
})
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct InstallAppDnaPayload {
pub hash: DnaHash,
pub role_name: RoleName,
pub membrane_proof: Option<MembraneProof>,
}
impl InstallAppDnaPayload {
pub fn hash_only(hash: DnaHash, role_name: RoleName) -> Self {
Self {
hash,
role_name,
membrane_proof: None,
}
}
}
#[derive(Clone, Debug, Into, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct InstalledCell {
cell_id: CellId,
role_name: RoleName,
}
impl InstalledCell {
pub fn new(cell_id: CellId, role_name: RoleName) -> Self {
Self { cell_id, role_name }
}
pub fn into_inner(self) -> (CellId, RoleName) {
(self.cell_id, self.role_name)
}
}
#[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_enabled(app: InstalledAppCommon) -> Self {
Self {
app,
status: AppStatus::Enabled,
}
}
pub fn status(&self) -> &AppStatus {
&self.status
}
pub fn id(&self) -> &InstalledAppId {
&self.app.installed_app_id
}
}
pub type InstalledAppMap = IndexMap<InstalledAppId, InstalledApp>;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct InstalledAppCommon {
pub installed_app_id: InstalledAppId,
pub agent_key: AgentPubKey,
pub role_assignments: IndexMap<RoleName, AppRoleAssignment>,
pub manifest: AppManifest,
pub installed_at: Timestamp,
}
impl InstalledAppCommon {
pub fn new<S: ToString, I: IntoIterator<Item = (RoleName, AppRoleAssignment)>>(
installed_app_id: S,
agent_key: AgentPubKey,
role_assignments: I,
manifest: AppManifest,
installed_at: Timestamp,
) -> AppResult<Self> {
let role_assignments = role_assignments.into_iter().collect::<Vec<_>>();
let duplicate_role_names = role_assignments
.iter()
.map(|(role_name, _)| role_name.to_owned())
.counts()
.into_iter()
.filter_map(|(role_name, count)| if count > 1 { Some(role_name) } else { None })
.collect::<Vec<RoleName>>();
if !duplicate_role_names.is_empty() {
return Err(AppError::DuplicateRoleNames(
installed_app_id.to_string(),
duplicate_role_names,
));
}
let role_assignments = role_assignments.into_iter().collect::<IndexMap<_, _>>();
if let Some((illegal_role_name, _)) = role_assignments
.iter()
.find(|(role_name, _)| role_name.contains(CLONE_ID_DELIMITER))
{
return Err(AppError::IllegalRoleName(illegal_role_name.clone()));
}
Ok(InstalledAppCommon {
installed_app_id: installed_app_id.to_string(),
agent_key,
role_assignments,
manifest,
installed_at,
})
}
pub fn id(&self) -> &InstalledAppId {
&self.installed_app_id
}
pub fn provisioned_cells(&self) -> impl Iterator<Item = (&RoleName, CellId)> {
self.role_assignments
.iter()
.filter_map(|(role_name, role)| {
role.provisioned_dna_hash()
.map(|d| (role_name, CellId::new(d.clone(), self.agent_key.clone())))
})
}
pub fn clone_cells(&self) -> impl Iterator<Item = (&CloneId, CellId)> {
self.role_assignments
.iter()
.flat_map(|app_role_assignment| {
app_role_assignment
.1
.as_primary()
.into_iter()
.flat_map(|p| {
p.clones
.iter()
.map(|(id, d)| (id, CellId::new(d.clone(), self.agent_key.clone())))
})
})
}
pub fn disabled_clone_cells(&self) -> impl Iterator<Item = (&CloneId, CellId)> {
self.role_assignments
.iter()
.flat_map(|app_role_assignment| {
app_role_assignment
.1
.as_primary()
.into_iter()
.flat_map(|p| {
p.disabled_clones
.iter()
.map(|(id, d)| (id, CellId::new(d.clone(), self.agent_key.clone())))
})
})
}
pub fn clone_cells_for_role_name(
&self,
role_name: &RoleName,
) -> Option<impl Iterator<Item = (&CloneId, CellId)>> {
Some(
self.role_assignments
.get(role_name)?
.as_primary()?
.clones
.iter()
.map(|(id, dna_hash)| (id, CellId::new(dna_hash.clone(), self.agent_key.clone()))),
)
}
pub fn disabled_clone_cells_for_role_name(
&self,
role_name: &RoleName,
) -> Option<impl Iterator<Item = (&CloneId, CellId)>> {
Some(
self.role_assignments
.get(role_name)?
.as_primary()?
.disabled_clones
.iter()
.map(|(id, dna_hash)| (id, CellId::new(dna_hash.clone(), self.agent_key.clone()))),
)
}
pub fn clone_cell_ids(&self) -> impl Iterator<Item = CellId> + '_ {
self.clone_cells().map(|(_, cell_id)| cell_id)
}
pub fn disabled_clone_cell_ids(&self) -> impl Iterator<Item = CellId> + '_ {
self.disabled_clone_cells().map(|(_, cell_id)| cell_id)
}
pub fn all_cells(&self) -> impl Iterator<Item = CellId> + '_ {
self.provisioned_cells()
.map(|(_, c)| c)
.chain(self.clone_cell_ids())
.chain(self.disabled_clone_cell_ids())
}
pub fn all_enabled_cells(&self) -> impl Iterator<Item = CellId> + '_ {
self.provisioned_cells()
.map(|(_, c)| c)
.chain(self.clone_cell_ids())
}
pub fn required_cells(&self) -> impl Iterator<Item = CellId> + '_ {
self.provisioned_cells().map(|(_, c)| c)
}
pub fn role(&self, role_name: &RoleName) -> AppResult<&AppRoleAssignment> {
self.role_assignments
.get(role_name)
.ok_or_else(|| AppError::RoleNameMissing(role_name.clone()))
}
pub fn primary_role(&self, role_name: &RoleName) -> AppResult<&AppRolePrimary> {
let app_id = self.installed_app_id.clone();
self.role(role_name)?
.as_primary()
.ok_or_else(|| AppError::NonPrimaryCell(app_id, role_name.clone()))
}
fn role_mut(&mut self, role_name: &RoleName) -> AppResult<&mut AppRoleAssignment> {
self.role_assignments
.get_mut(role_name)
.ok_or_else(|| AppError::RoleNameMissing(role_name.clone()))
}
fn primary_role_mut(&mut self, role_name: &RoleName) -> AppResult<&mut AppRolePrimary> {
let app_id = self.installed_app_id.clone();
self.role_mut(role_name)?
.as_primary_mut()
.ok_or_else(|| AppError::NonPrimaryCell(app_id, role_name.clone()))
}
pub fn roles(&self) -> &IndexMap<RoleName, AppRoleAssignment> {
&self.role_assignments
}
pub fn primary_roles(&self) -> impl Iterator<Item = (&RoleName, &AppRolePrimary)> {
self.role_assignments
.iter()
.filter_map(|(name, role)| Some((name, role.as_primary()?)))
}
pub fn add_clone(&mut self, role_name: &RoleName, dna_hash: &DnaHash) -> AppResult<CloneId> {
let app_role_assignment = self.primary_role_mut(role_name)?;
if app_role_assignment.is_clone_limit_reached() {
return Err(AppError::CloneLimitExceeded(
app_role_assignment.clone_limit,
Box::new(app_role_assignment.clone()),
));
}
let clone_id = CloneId::new(role_name, app_role_assignment.next_clone_index);
if app_role_assignment.clones.contains_key(&clone_id) {
return Err(AppError::DuplicateCloneIds(clone_id));
}
app_role_assignment
.clones
.insert(clone_id.clone(), dna_hash.clone());
app_role_assignment.next_clone_index += 1;
Ok(clone_id)
}
pub fn get_clone_dna_hash(&self, clone_cell_id: &CloneCellId) -> AppResult<DnaHash> {
let cell_id = match clone_cell_id {
CloneCellId::DnaHash(dna_hash) => dna_hash,
CloneCellId::CloneId(clone_id) => self
.primary_role(&clone_id.as_base_role_name())?
.clones
.get(clone_id)
.ok_or_else(|| {
AppError::CloneCellNotFound(CloneCellId::CloneId(clone_id.clone()))
})?,
};
Ok(cell_id.clone())
}
pub fn get_clone_id(&self, clone_cell_id: &CloneCellId) -> AppResult<CloneId> {
let clone_id = match clone_cell_id {
CloneCellId::CloneId(id) => id.clone(),
CloneCellId::DnaHash(id) => self
.role_assignments
.iter()
.flat_map(|(_, role_assignment)| {
role_assignment
.as_primary()
.into_iter()
.flat_map(|r| r.disabled_clones.iter().chain(r.clones.iter()))
})
.find(|(_, cell_id)| *cell_id == id)
.ok_or_else(|| AppError::CloneCellNotFound(CloneCellId::DnaHash(id.clone())))?
.0
.clone(),
};
Ok(clone_id)
}
pub fn disable_clone_cell(&mut self, clone_id: &CloneId) -> AppResult<()> {
let app_role_assignment = self.primary_role_mut(&clone_id.as_base_role_name())?;
match app_role_assignment.clones.remove(clone_id) {
None => {
if app_role_assignment.disabled_clones.contains_key(clone_id) {
Ok(())
} else {
Err(AppError::CloneCellNotFound(CloneCellId::CloneId(
clone_id.to_owned(),
)))
}
}
Some(cell_id) => {
let insert_result = app_role_assignment
.disabled_clones
.insert(clone_id.to_owned(), cell_id);
assert!(
insert_result.is_none(),
"disable: clone cell is already disabled"
);
Ok(())
}
}
}
pub fn enable_clone_cell(&mut self, clone_id: &CloneId) -> AppResult<InstalledCell> {
let app_role_assignment = self.primary_role_mut(&clone_id.as_base_role_name())?;
match app_role_assignment.disabled_clones.remove(clone_id) {
None => app_role_assignment
.clones
.get(clone_id)
.cloned()
.map(|dna_hash| {
Ok(InstalledCell {
role_name: clone_id.as_app_role_name().to_owned(),
cell_id: CellId::new(dna_hash, self.agent_key.clone()),
})
})
.unwrap_or_else(|| {
Err(AppError::CloneCellNotFound(CloneCellId::CloneId(
clone_id.to_owned(),
)))
}),
Some(dna_hash) => {
let insert_result = app_role_assignment
.clones
.insert(clone_id.to_owned(), dna_hash.clone());
assert!(
insert_result.is_none(),
"enable: clone cell already enabled"
);
Ok(InstalledCell {
role_name: clone_id.as_app_role_name().to_owned(),
cell_id: CellId::new(dna_hash, self.agent_key.clone()),
})
}
}
}
pub fn delete_clone_cell(&mut self, clone_id: &CloneId) -> AppResult<()> {
let app_role_assignment = self.primary_role_mut(&clone_id.as_base_role_name())?;
app_role_assignment
.disabled_clones
.remove(clone_id)
.map(|_| ())
.ok_or_else(|| {
if app_role_assignment.clones.contains_key(clone_id) {
AppError::CloneCellMustBeDisabledBeforeDeleting(CloneCellId::CloneId(
clone_id.to_owned(),
))
} else {
AppError::CloneCellNotFound(CloneCellId::CloneId(clone_id.to_owned()))
}
})
}
pub fn agent_key(&self) -> &AgentPubKey {
&self.agent_key
}
pub fn manifest(&self) -> &AppManifest {
&self.manifest
}
pub fn role_assignments(&self) -> &IndexMap<RoleName, AppRoleAssignment> {
&self.role_assignments
}
pub fn installed_at(&self) -> &Timestamp {
&self.installed_at
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub struct WarrantSummary {
pub author: AgentPubKey,
pub warrantee: AgentPubKey,
pub timestamp: Timestamp,
}
impl From<SignedWarrant> for WarrantSummary {
fn from(sw: SignedWarrant) -> Self {
let w = sw.into_data();
Self {
author: w.author,
warrantee: w.warrantee,
timestamp: w.timestamp,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub enum UnrecoverableCellReason {
ChainForkWarrant(Box<WarrantSummary>),
ChainIntegrityWarrant(Box<WarrantSummary>),
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub enum AppStatus {
Enabled,
Disabled(DisabledAppReason),
AwaitingMemproofs,
AwaitingRestore,
Unrecoverable(CellId, UnrecoverableCellReason),
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export, export_to = "api/admin/types.ts"))]
pub enum DisabledAppReason {
NeverStarted,
NotStartedAfterProvidingMemproofs,
User,
Error(String),
}
impl From<DisabledAppReason> for AppStatus {
fn from(reason: DisabledAppReason) -> Self {
match reason {
DisabledAppReason::NeverStarted => Self::Disabled(reason),
DisabledAppReason::NotStartedAfterProvidingMemproofs => {
Self::Disabled(DisabledAppReason::NotStartedAfterProvidingMemproofs)
}
DisabledAppReason::Error(err) => Self::Disabled(DisabledAppReason::Error(err)),
DisabledAppReason::User => Self::Disabled(DisabledAppReason::User),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, derive_more::From)]
pub enum AppRoleAssignment {
Primary(AppRolePrimary),
Dependency(AppRoleDependency),
}
impl AppRoleAssignment {
pub fn as_primary(&self) -> Option<&AppRolePrimary> {
match self {
Self::Primary(p) => Some(p),
Self::Dependency(_) => None,
}
}
pub fn as_primary_mut(&mut self) -> Option<&mut AppRolePrimary> {
match self {
Self::Primary(p) => Some(p),
Self::Dependency(_) => None,
}
}
pub fn provisioned_dna_hash(&self) -> Option<&DnaHash> {
match self {
Self::Primary(p) => p.provisioned_dna_hash(),
Self::Dependency(_) => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AppRolePrimary {
pub base_dna_hash: DnaHash,
pub is_provisioned: bool,
pub clone_limit: u32,
pub next_clone_index: u32,
pub clones: HashMap<CloneId, DnaHash>,
pub disabled_clones: HashMap<CloneId, DnaHash>,
}
impl AppRolePrimary {
pub fn new(base_dna_hash: DnaHash, is_provisioned: bool, clone_limit: u32) -> Self {
Self {
base_dna_hash,
is_provisioned,
clone_limit,
clones: HashMap::new(),
next_clone_index: 0,
disabled_clones: HashMap::new(),
}
}
pub fn dna_hash(&self) -> &DnaHash {
&self.base_dna_hash
}
pub fn provisioned_dna_hash(&self) -> Option<&DnaHash> {
if self.is_provisioned {
Some(&self.base_dna_hash)
} else {
None
}
}
pub fn clone_limit(&self) -> u32 {
self.clone_limit
}
pub fn is_clone_limit_reached(&self) -> bool {
self.clones.len() as u32 == self.clone_limit
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AppRoleDependency {
pub cell_id: CellId,
pub protected: bool,
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use ::fixt::prelude::*;
use holo_hash::fixt::*;
use serde_json;
use std::collections::HashSet;
#[test]
fn illegal_role_name_is_rejected() {
let result = InstalledAppCommon::new(
"test_app",
fixt!(AgentPubKey),
vec![(
CLONE_ID_DELIMITER.into(),
AppRolePrimary::new(fixt!(DnaHash), false, 0).into(),
)],
AppManifest::V0(AppManifestV0 {
name: "test_app".to_string(),
description: None,
roles: vec![],
allow_deferred_memproofs: false,
relay_url: None,
bootstrap_url: None,
}),
Timestamp::now(),
);
assert!(result.is_err())
}
#[test]
fn clone_management() {
let base_dna_hash = fixt!(DnaHash);
let new_clone = || fixt!(DnaHash);
let clone_limit = 3;
let role1 = AppRolePrimary::new(base_dna_hash, false, clone_limit).into();
let agent = fixt!(AgentPubKey);
let role_name: RoleName = "role_name".into();
let manifest = AppManifest::V0(AppManifestV0 {
name: "test_app".to_string(),
description: None,
roles: vec![],
allow_deferred_memproofs: false,
relay_url: None,
bootstrap_url: None,
});
let mut app = InstalledAppCommon::new(
"app",
agent.clone(),
vec![(role_name.clone(), role1)],
manifest,
Timestamp::now(),
)
.unwrap();
let clones: Vec<_> = vec![new_clone(), new_clone(), new_clone()];
let clone_id_0 = app.add_clone(&role_name, &clones[0]).unwrap();
let clone_id_1 = app.add_clone(&role_name, &clones[1]).unwrap();
let clone_id_2 = app.add_clone(&role_name, &clones[2]).unwrap();
assert_eq!(clone_id_0, CloneId::new(&role_name, 0));
assert_eq!(clone_id_1, CloneId::new(&role_name, 1));
assert_eq!(clone_id_2, CloneId::new(&role_name, 2));
assert_eq!(
app.clone_cell_ids()
.map(|id| id.dna_hash().clone())
.collect::<HashSet<_>>(),
clones.clone().into_iter().collect::<HashSet<_>>()
);
assert_eq!(app.clone_cells().count(), 3);
let result_add_clone_twice = app.add_clone(&role_name, &clones[0]);
assert!(result_add_clone_twice.is_err());
matches::assert_matches!(
app.add_clone(&role_name, &new_clone()),
Err(AppError::CloneLimitExceeded(3, _))
);
app.disable_clone_cell(&clone_id_0).unwrap();
assert!(!app
.clone_cells()
.any(|(clone_id, _)| *clone_id == clone_id_0));
assert_eq!(app.clone_cells().count(), 2);
assert!(app
.disabled_clone_cells()
.any(|(clone_id, _)| *clone_id == clone_id_0));
let enabled_cell = app.enable_clone_cell(&clone_id_0).unwrap();
assert_eq!(
enabled_cell.role_name,
clone_id_0.as_app_role_name().to_owned()
);
let enabled_cell_2 = app.enable_clone_cell(&clone_id_0).unwrap();
assert_eq!(enabled_cell_2, enabled_cell);
assert!(app
.clone_cells()
.any(|(clone_id, _)| *clone_id == clone_id_0));
assert_eq!(
app.clone_cell_ids()
.map(|id| id.dna_hash().clone())
.collect::<HashSet<_>>(),
clones.clone().into_iter().collect::<HashSet<_>>()
);
assert_eq!(app.clone_cells().count(), 3);
app.disable_clone_cell(&clone_id_0).unwrap();
app.disable_clone_cell(&clone_id_0).unwrap();
app.delete_clone_cell(&clone_id_0).unwrap();
assert!(app.enable_clone_cell(&clone_id_0).is_err());
}
#[test]
fn get_clone_id_works_for_enabled_and_disabled_clones() {
let base_dna_hash = fixt!(DnaHash);
let new_clone = || fixt!(DnaHash);
let clone_limit = 3;
let role1 = AppRolePrimary::new(base_dna_hash, false, clone_limit).into();
let agent = fixt!(AgentPubKey);
let role_name: RoleName = "role_name".into();
let manifest = AppManifest::V0(AppManifestV0 {
name: "test_app".to_string(),
description: None,
roles: vec![],
allow_deferred_memproofs: false,
bootstrap_url: None,
relay_url: None,
});
let mut app = InstalledAppCommon::new(
"app",
agent.clone(),
vec![(role_name.clone(), role1)],
manifest,
Timestamp::now(),
)
.unwrap();
let clone_dna_hash = new_clone();
let clone_id = app.add_clone(&role_name, &clone_dna_hash).unwrap();
let result_by_clone_id = app.get_clone_id(&CloneCellId::CloneId(clone_id.clone()));
assert!(result_by_clone_id.is_ok());
assert_eq!(result_by_clone_id.unwrap(), clone_id);
let result_by_dna_hash = app.get_clone_id(&CloneCellId::DnaHash(clone_dna_hash.clone()));
assert!(result_by_dna_hash.is_ok());
assert_eq!(result_by_dna_hash.unwrap(), clone_id);
app.disable_clone_cell(&clone_id).unwrap();
let result_disabled_by_id = app.get_clone_id(&CloneCellId::CloneId(clone_id.clone()));
assert!(result_disabled_by_id.is_ok());
assert_eq!(result_disabled_by_id.unwrap(), clone_id);
let result_disabled_by_hash = app.get_clone_id(&CloneCellId::DnaHash(clone_dna_hash));
assert!(result_disabled_by_hash.is_ok());
assert_eq!(result_disabled_by_hash.unwrap(), clone_id);
}
#[test]
fn dna_source_serialization() {
use serde_json;
let dna_source: DnaSource = DnaSource::Path("is the goal".into());
assert_eq!(
serde_json::to_string(&dna_source).unwrap(),
"{\"type\":\"path\",\"value\":\"is the goal\"}"
);
}
#[test]
fn coordinator_source_serialization() {
let coordinator_source: CoordinatorSource = CoordinatorSource::Path("is the goal".into());
assert_eq!(
serde_json::to_string(&coordinator_source).unwrap(),
"{\"type\":\"path\",\"value\":\"is the goal\"}"
);
}
#[test]
fn role_settings_serialization() {
let role_settings: RoleSettings = RoleSettings::Provisioned {
membrane_proof: None,
modifiers: None,
init_properties: None,
};
assert_eq!(
serde_json::to_string(&role_settings).unwrap(),
"{\"type\":\"provisioned\",\"value\":{\"membrane_proof\":null,\"modifiers\":null,\"init_properties\":null}}"
);
}
#[test]
fn app_bundle_source_serialization() {
let app_bundle_source: AppBundleSource = AppBundleSource::Path("is the goal".into());
assert_eq!(
serde_json::to_string(&app_bundle_source).unwrap(),
"{\"type\":\"path\",\"value\":\"is the goal\"}"
);
}
#[test]
fn app_status_serialization() {
let app_status: AppStatus = AppStatus::Enabled;
assert_eq!(
serde_json::to_string(&app_status).unwrap(),
"{\"type\":\"enabled\"}"
);
let app_status: AppStatus = AppStatus::Disabled(DisabledAppReason::NeverStarted);
assert_eq!(
serde_json::to_string(&app_status).unwrap(),
"{\"type\":\"disabled\",\"value\":{\"type\":\"never_started\"}}"
);
}
#[test]
fn disabled_app_reason_serialization() {
let reason = DisabledAppReason::User;
assert_eq!(
serde_json::to_string(&reason).unwrap(),
"{\"type\":\"user\"}"
);
}
#[test]
fn warrant_summary_serde_round_trip() {
let summary = WarrantSummary {
author: fixt!(AgentPubKey),
warrantee: fixt!(AgentPubKey),
timestamp: Timestamp::from_micros(1_000_000),
};
let bytes = SerializedBytes::try_from(&summary).unwrap();
let recovered: WarrantSummary = bytes.try_into().unwrap();
assert_eq!(summary, recovered);
}
#[test]
fn unrecoverable_cell_reason_serde_round_trip() {
let summary = WarrantSummary {
author: fixt!(AgentPubKey),
warrantee: fixt!(AgentPubKey),
timestamp: Timestamp::from_micros(1_000_000),
};
let reason = UnrecoverableCellReason::ChainForkWarrant(Box::new(summary));
let bytes = SerializedBytes::try_from(&reason).unwrap();
let recovered: UnrecoverableCellReason = bytes.try_into().unwrap();
assert_eq!(reason, recovered);
}
#[test]
fn app_status_awaiting_restore_serialization() {
let status = AppStatus::AwaitingRestore;
assert_eq!(
serde_json::to_string(&status).unwrap(),
r#"{"type":"awaiting_restore"}"#
);
let recovered: AppStatus = serde_json::from_str(r#"{"type":"awaiting_restore"}"#).unwrap();
assert_eq!(status, recovered);
}
#[test]
fn app_status_unrecoverable_serialization() {
let cell_id = CellId::new(fixt!(DnaHash), fixt!(AgentPubKey));
let summary = WarrantSummary {
author: fixt!(AgentPubKey),
warrantee: fixt!(AgentPubKey),
timestamp: Timestamp::from_micros(1_000_000),
};
let reason = UnrecoverableCellReason::ChainForkWarrant(Box::new(summary));
let status = AppStatus::Unrecoverable(cell_id, reason);
let json = serde_json::to_string(&status).unwrap();
let recovered: AppStatus = serde_json::from_str(&json).unwrap();
assert_eq!(status, recovered);
}
}