use crate::action::conversions::WrongActionError;
use crate::action::{Action, ActionData, ActionType, EntryType};
use crate::entry::Entry;
use crate::record::{Record, SignedHashed};
use holo_hash::{ActionHash, AgentPubKey, EntryHash};
use holochain_serialized_bytes::prelude::*;
use holochain_timestamp::Timestamp;
pub trait UnitEnum {
type Unit: core::fmt::Debug
+ Clone
+ Copy
+ PartialEq
+ Eq
+ PartialOrd
+ Ord
+ core::hash::Hash;
fn to_unit(&self) -> Self::Unit;
fn unit_iter() -> Box<dyn Iterator<Item = Self::Unit>>;
}
impl UnitEnum for () {
type Unit = ();
fn to_unit(&self) -> Self::Unit {}
fn unit_iter() -> Box<dyn Iterator<Item = Self::Unit>> {
Box::new([].into_iter())
}
}
#[derive(Clone, Debug)]
pub enum UnitEnumEither<E: UnitEnum> {
Enum(E),
Unit(E::Unit),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SerializedBytes)]
pub enum Op {
CreateRecord(CreateRecord),
CreateEntry(CreateEntry),
Update(Update),
Delete(Delete),
AgentActivity(AgentActivity),
CreateLink(CreateLink),
DeleteLink(DeleteLink),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SerializedBytes)]
pub struct CreateRecord {
pub record: Record,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes)]
pub struct CreateEntry {
pub action: SignedHashed<Action>,
pub entry: Entry,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes)]
pub struct Update {
pub update: SignedHashed<Action>,
pub new_entry: Option<Entry>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes)]
pub struct Delete {
pub delete: SignedHashed<Action>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "hdk/action.ts"))]
pub struct AgentActivity {
pub action: SignedHashed<Action>,
pub cached_entry: Option<Entry>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes)]
pub struct CreateLink {
pub create_link: SignedHashed<Action>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SerializedBytes)]
pub struct DeleteLink {
pub delete_link: SignedHashed<Action>,
pub create_link: Action,
}
impl CreateEntry {
pub fn new(action: SignedHashed<Action>, entry: Entry) -> Result<Self, WrongActionError> {
match &action.hashed.content.data {
ActionData::Create(_) | ActionData::Update(_) => Ok(Self { action, entry }),
other => Err(WrongActionError(format!(
"CreateEntry requires Create or Update action data, got {:?}",
other.action_type()
))),
}
}
}
impl Update {
pub fn new(
update: SignedHashed<Action>,
new_entry: Option<Entry>,
) -> Result<Self, WrongActionError> {
match &update.hashed.content.data {
ActionData::Update(_) => Ok(Self { update, new_entry }),
other => Err(WrongActionError(format!(
"Update requires Update action data, got {:?}",
other.action_type()
))),
}
}
}
impl Delete {
pub fn new(delete: SignedHashed<Action>) -> Result<Self, WrongActionError> {
match &delete.hashed.content.data {
ActionData::Delete(_) => Ok(Self { delete }),
other => Err(WrongActionError(format!(
"Delete requires Delete action data, got {:?}",
other.action_type()
))),
}
}
}
impl CreateLink {
pub fn new(create_link: SignedHashed<Action>) -> Result<Self, WrongActionError> {
match &create_link.hashed.content.data {
ActionData::CreateLink(_) => Ok(Self { create_link }),
other => Err(WrongActionError(format!(
"CreateLink requires CreateLink action data, got {:?}",
other.action_type()
))),
}
}
}
impl DeleteLink {
pub fn new(
delete_link: SignedHashed<Action>,
create_link: Action,
) -> Result<Self, WrongActionError> {
match (&delete_link.hashed.content.data, &create_link.data) {
(ActionData::DeleteLink(dl), ActionData::CreateLink(cl)) => {
if dl.base_address != cl.base_address {
return Err(WrongActionError(
"DeleteLink requires the DeleteLink and CreateLink to share a base address"
.into(),
));
}
#[cfg(feature = "hashing")]
{
use crate::action::ActionHashed;
use holo_hash::HasHash;
let create_link_hash =
ActionHashed::from_content_sync(create_link.clone()).into_hash();
if create_link_hash != dl.link_add_address {
return Err(WrongActionError(format!(
"DeleteLink requires the CreateLink action referenced by link_add_address ({}), got a CreateLink action hashing to {}",
dl.link_add_address, create_link_hash
)));
}
}
Ok(Self {
delete_link,
create_link,
})
}
(dl, cl) => Err(WrongActionError(format!(
"DeleteLink requires DeleteLink and CreateLink action data, got {:?} and {:?}",
dl.action_type(),
cl.action_type()
))),
}
}
}
impl Op {
fn signed_action(&self) -> &SignedHashed<Action> {
match self {
Op::CreateRecord(CreateRecord { record }) => &record.signed_action,
Op::CreateEntry(CreateEntry { action, .. }) => action,
Op::Update(Update { update, .. }) => update,
Op::Delete(Delete { delete }) => delete,
Op::AgentActivity(AgentActivity { action, .. }) => action,
Op::CreateLink(CreateLink { create_link }) => create_link,
Op::DeleteLink(DeleteLink { delete_link, .. }) => delete_link,
}
}
pub fn author(&self) -> &AgentPubKey {
&self.signed_action().hashed.content.header.author
}
pub fn timestamp(&self) -> Timestamp {
self.signed_action().hashed.content.header.timestamp
}
pub fn action_seq(&self) -> u32 {
self.signed_action().hashed.content.header.action_seq
}
pub fn prev_action(&self) -> Option<&ActionHash> {
self.signed_action()
.hashed
.content
.header
.prev_action
.as_ref()
}
pub fn action_type(&self) -> ActionType {
self.signed_action().hashed.content.data.action_type()
}
pub fn action_hash(&self) -> &ActionHash {
self.signed_action().as_hash()
}
pub fn entry_data(&self) -> Option<(&EntryHash, &EntryType)> {
match &self.signed_action().hashed.content.data {
ActionData::Create(d) => Some((&d.entry_hash, &d.entry_type)),
ActionData::Update(d) => Some((&d.entry_hash, &d.entry_type)),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::action::{
Action, ActionData, ActionHashed, ActionHeader, CreateData, DeleteData, DeleteLinkData,
};
use crate::record::SignedHashed;
use crate::signature::Signature;
use holo_hash::{ActionHash, AgentPubKey, EntryHash, HasHash, HoloHashed};
fn signed_action(data: ActionData) -> SignedHashed<Action> {
let action = Action {
header: ActionHeader {
author: AgentPubKey::from_raw_36(vec![1u8; 36]),
timestamp: holochain_timestamp::Timestamp::from_micros(7),
action_seq: 1,
prev_action: Some(ActionHash::from_raw_36(vec![2u8; 36])),
},
data,
};
let hash = ActionHash::from_raw_36(vec![9u8; 36]);
SignedHashed::with_presigned(
HoloHashed::with_pre_hashed(action, hash),
Signature([0u8; 64]),
)
}
fn create_data() -> ActionData {
ActionData::Create(CreateData {
entry_type: EntryType::AgentPubKey,
entry_hash: EntryHash::from_raw_36(vec![3u8; 36]),
})
}
fn delete_data() -> ActionData {
ActionData::Delete(DeleteData {
deletes_address: ActionHash::from_raw_36(vec![4u8; 36]),
deletes_entry_address: EntryHash::from_raw_36(vec![5u8; 36]),
})
}
fn delete_link_data() -> ActionData {
ActionData::DeleteLink(DeleteLinkData {
base_address: EntryHash::from_raw_36(vec![6u8; 36]).into(),
link_add_address: ActionHash::from_raw_36(vec![7u8; 36]),
})
}
#[test]
fn store_entry_accepts_create_and_update() {
let entry = Entry::Agent(AgentPubKey::from_raw_36(vec![1u8; 36]));
assert!(CreateEntry::new(signed_action(create_data()), entry.clone()).is_ok());
let update = ActionData::Update(crate::action::UpdateData {
original_action_address: ActionHash::from_raw_36(vec![10u8; 36]),
original_entry_address: EntryHash::from_raw_36(vec![11u8; 36]),
entry_type: EntryType::AgentPubKey,
entry_hash: EntryHash::from_raw_36(vec![12u8; 36]),
});
assert!(CreateEntry::new(signed_action(update), entry).is_ok());
}
#[test]
fn store_entry_rejects_non_entry_action() {
let entry = Entry::Agent(AgentPubKey::from_raw_36(vec![1u8; 36]));
assert!(CreateEntry::new(signed_action(delete_data()), entry).is_err());
}
#[test]
fn register_delete_rejects_non_delete() {
assert!(Delete::new(signed_action(create_data())).is_err());
assert!(Delete::new(signed_action(delete_data())).is_ok());
}
fn create_link_action(base: u8, target: u8) -> Action {
Action {
header: ActionHeader {
author: AgentPubKey::from_raw_36(vec![1u8; 36]),
timestamp: holochain_timestamp::Timestamp::from_micros(1),
action_seq: 0,
prev_action: None,
},
data: ActionData::CreateLink(crate::action::CreateLinkData {
base_address: EntryHash::from_raw_36(vec![base; 36]).into(),
target_address: EntryHash::from_raw_36(vec![target; 36]).into(),
zome_index: crate::action::ZomeIndex(0),
link_type: crate::link::LinkType(0),
tag: crate::link::LinkTag(vec![]),
}),
}
}
#[test]
fn register_delete_link_requires_delete_link_and_create_link() {
let create_link = create_link_action(6, 8);
let create_link_hash = ActionHashed::from_content_sync(create_link.clone()).into_hash();
let delete_link_data = ActionData::DeleteLink(DeleteLinkData {
base_address: EntryHash::from_raw_36(vec![6u8; 36]).into(),
link_add_address: create_link_hash,
});
assert!(DeleteLink::new(signed_action(delete_link_data), create_link).is_ok());
}
#[test]
fn register_delete_link_rejects_mismatched_base_address() {
let create_link = create_link_action(9, 8);
assert!(DeleteLink::new(signed_action(delete_link_data()), create_link).is_err());
}
#[test]
fn register_delete_link_rejects_matching_base_but_wrong_hash() {
let create_link = create_link_action(6, 8);
assert_ne!(
ActionHashed::from_content_sync(create_link.clone()).into_hash(),
match delete_link_data() {
ActionData::DeleteLink(DeleteLinkData {
link_add_address, ..
}) => link_add_address,
_ => unreachable!(),
}
);
assert!(DeleteLink::new(signed_action(delete_link_data()), create_link).is_err());
}
#[test]
fn op_accessors_read_header_and_data() {
let sah = signed_action(create_data());
let expected_hash = sah.as_hash().clone();
let op = Op::AgentActivity(AgentActivity {
action: sah,
cached_entry: None,
});
assert_eq!(op.action_seq(), 1);
assert_eq!(op.author(), &AgentPubKey::from_raw_36(vec![1u8; 36]));
assert_eq!(
op.timestamp(),
holochain_timestamp::Timestamp::from_micros(7)
);
assert_eq!(
op.prev_action(),
Some(&ActionHash::from_raw_36(vec![2u8; 36]))
);
assert_eq!(op.action_type(), crate::action::ActionType::Create);
assert_eq!(op.action_hash(), &expected_hash);
let (entry_hash, entry_type) = op.entry_data().expect("create has entry data");
assert_eq!(entry_hash, &EntryHash::from_raw_36(vec![3u8; 36]));
assert_eq!(entry_type, &EntryType::AgentPubKey);
}
#[test]
fn op_entry_data_none_for_delete() {
let op = Op::Delete(Delete::new(signed_action(delete_data())).unwrap());
assert!(op.entry_data().is_none());
}
#[test]
fn op_serde_roundtrip() {
let entry = Entry::Agent(AgentPubKey::from_raw_36(vec![1u8; 36]));
let store_entry =
Op::CreateEntry(CreateEntry::new(signed_action(create_data()), entry).unwrap());
let store_record = Op::CreateRecord(CreateRecord {
record: Record::new(signed_action(create_data()), crate::record::RecordEntry::NA),
});
for op in [store_entry, store_record] {
let bytes = holochain_serialized_bytes::encode(&op).unwrap();
let decoded: Op = holochain_serialized_bytes::decode(&bytes).unwrap();
assert_eq!(decoded, op);
}
}
#[test]
fn op_accessors_work_through_store_record() {
let sah = signed_action(create_data());
let expected_hash = sah.as_hash().clone();
let record = Record::new(sah, crate::record::RecordEntry::NA);
let op = Op::CreateRecord(CreateRecord { record });
assert_eq!(op.action_hash(), &expected_hash);
assert_eq!(op.action_seq(), 1);
}
#[test]
fn op_entry_data_some_for_update() {
let update = ActionData::Update(crate::action::UpdateData {
original_action_address: ActionHash::from_raw_36(vec![10u8; 36]),
original_entry_address: EntryHash::from_raw_36(vec![11u8; 36]),
entry_type: EntryType::AgentPubKey,
entry_hash: EntryHash::from_raw_36(vec![12u8; 36]),
});
let op = Op::Update(Update::new(signed_action(update), None).unwrap());
let (entry_hash, entry_type) = op.entry_data().expect("update has entry data");
assert_eq!(entry_hash, &EntryHash::from_raw_36(vec![12u8; 36]));
assert_eq!(entry_type, &EntryType::AgentPubKey);
}
}