hdi_extensions/
lib.rs

1mod macros;
2
3pub use hdi;
4pub use holo_hash;
5
6use core::convert::{ TryFrom, TryInto };
7use hdi::prelude::{
8    must_get_action,
9    must_get_entry,
10    must_get_valid_record,
11    ExternResult, WasmError, WasmErrorInner,
12    Deserialize, Serialize, SerializedBytesError,
13    ActionHash, EntryHash, ExternalHash, AnyDhtHash, AnyLinkableHash,
14    Record, Action, Entry, EntryCreationAction, ActionType,
15    SignedActionHashed, EntryHashed,
16    AppEntryDef, ScopedEntryDefIndex,
17    EntryType, EntryTypesHelper,
18    // Action Types
19    Dna, AgentValidationPkg, InitZomesComplete,
20    CreateLink, DeleteLink, OpenChain, CloseChain,
21    Create, Update, Delete,
22};
23use holo_hash::AnyLinkableHashPrimitive;
24
25
26// Prefix association train alternatives for 'must_'
27// - sure, precise, exact, rigid, strict, pure, hard, assured, ensured, secure, guaranteed
28//
29// In the end, 'pure_get' or any other prefixes for 'get' seemed to long.  'summon' implies that the
30// thing is not present but it exists somewhere else.
31//
32// However, 'must_get' also means that it must get the record even if it is deleted.
33/// Alias for [`must_get_valid_record`]
34pub fn summon_valid_record(action_hash: &ActionHash) -> ExternResult<Record> {
35    must_get_valid_record(action_hash.to_owned())
36}
37
38/// Alias for [`must_get_action`]
39pub fn summon_action(action_hash: &ActionHash) -> ExternResult<SignedActionHashed> {
40    must_get_action(action_hash.to_owned())
41}
42
43/// Alias for [`must_get_entry`]
44pub fn summon_entry(entry_hash: &EntryHash) -> ExternResult<EntryHashed> {
45    must_get_entry(entry_hash.to_owned())
46}
47
48
49//
50// Error Handling
51//
52/// Replace [`SerializedBytesError::Deserialize`] in [`WasmErrorInner::Serialize`] with [`WasmErrorInner::Guest`]
53fn convert_deserialize_error(error: WasmError) -> WasmError {
54    match error {
55        WasmError { error: WasmErrorInner::Serialize(SerializedBytesError::Deserialize(msg)), .. } =>
56            guest_error!(
57                format!("Could not deserialize any-linkable address to expected type: {}", msg )
58            ),
59        err => err,
60    }
61}
62
63
64//
65// Tracing Actions
66//
67/// Collect the chain of evolutions backwards
68pub fn trace_origin(action_address: &ActionHash) -> ExternResult<Vec<(ActionHash, Action)>> {
69    let mut history = vec![];
70    let mut next_addr = Some(action_address.to_owned());
71
72    while let Some(addr) = next_addr {
73        let record = summon_valid_record( &addr )?;
74
75        next_addr = match record.action() {
76            Action::Update(update) => Some(update.original_action_address.to_owned()),
77            Action::Create(_) => None,
78            _ => return Err(guest_error!(format!("Wrong action type '{}'", record.action().action_type() )))?,
79        };
80
81        history.push( (record.signed_action.hashed.hash, record.signed_action.hashed.content) );
82    }
83
84    Ok( history )
85}
86
87
88/// Get the last item in a [`trace_origin`] result
89///
90/// This should always be a [`Create`] action.
91pub fn trace_origin_root(action_address: &ActionHash) -> ExternResult<(ActionHash, Action)> {
92    Ok( trace_origin( action_address )?.last().unwrap().to_owned() )
93}
94
95
96//
97// Entry Struct
98//
99/// Methods for getting scoped-type info from an entry struct
100pub trait ScopedTypeConnector<T,U>
101where
102    ScopedEntryDefIndex: for<'a> TryFrom<&'a T, Error = WasmError>,
103{
104    /// Get this entry's corresponding unit enum
105    fn unit() -> U;
106    /// Get this entry's [`AppEntryDef`]
107    fn app_entry_def() -> AppEntryDef;
108    /// Check if a [`Record`]'s entry type matches this entry
109    fn check_record_entry_type(record: &Record) -> bool;
110    /// Deserialize a [`Record`]'s [`Entry`] into this struct
111    fn try_from_record(record: &Record) -> Result<Self, Self::Error>
112    where
113        Self: TryFrom<Record>;
114    /// Wrap this entry in the corresponding entry type enum
115    fn to_input(&self) -> T;
116}
117
118/// Defines [`ScopedTypeConnector`] methods for an entry type
119///
120/// Rule patterns
121/// - #1 - `<unit enum>::<unit name>, <types enum>( <entry struct> )`
122/// - #2 - `<unit enum>::<unit name>, <types enum>, <entry struct>`
123///
124/// ##### Example: Basic Usage
125/// ```ignore
126/// use hdi::prelude::*;
127/// use hdi_extensions::*;
128///
129/// #[hdk_entry_helper]
130/// struct PostEntry {
131///     pub message: String,
132/// }
133///
134/// #[hdk_entry_defs]
135/// #[unit_enum(EntryTypesUnit)]
136/// pub enum EntryTypes {
137///     #[entry_def]
138///     Post(PostEntry),
139/// }
140///
141/// scoped_type_connector!(
142///     EntryTypesUnit::Post,
143///     EntryTypes::Post( PostEntry )
144/// );
145/// ```
146#[macro_export]
147macro_rules! scoped_type_connector {
148    ($units:ident::$unit_name:ident, $types:ident::$name:ident( $entry:ident ) ) => {
149        scoped_type_connector!( $units::$unit_name, $types::$name, $entry );
150    };
151    ($units:ident::$unit_name:ident, $types:ident::$name:ident, $entry:ident ) => {
152        impl ScopedTypeConnector<$types,$units> for $entry {
153
154            fn unit() -> $units {
155                $units::$unit_name
156            }
157
158            fn app_entry_def () -> AppEntryDef {
159                // We know this is always defined because the hdi macros (hdk_entry_defs, unit_enum)
160                // ensure that there will be a corresponding entry type for each unit.
161                AppEntryDef::try_from( Self::unit() ).unwrap()
162            }
163
164            fn check_record_entry_type (record: &Record) -> bool {
165                match EntryCreationAction::try_from( record.action().to_owned() ) {
166                    Ok(creation_action) => match creation_action.entry_type() {
167                        EntryType::App(aed) => Self::app_entry_def() == *aed,
168                        _ => false,
169                    },
170                    _ => false,
171                }
172            }
173
174            /// This "try from" checks the record's `EntryType` to make sure it matches the expected
175            /// `AppEntryDef` and then uses the official `TryFrom<Record>`.
176            fn try_from_record (record: &Record) -> Result<Self, WasmError> {
177                let creation_action = EntryCreationAction::try_from( record.action().to_owned() )
178                    .map_err(|_| hdi_extensions::guest_error!(
179                        format!("ID does not belong to a Creation Action")
180                    ))?;
181
182                if let EntryType::App(aed) = creation_action.entry_type() {
183                    if Self::app_entry_def() == *aed {
184                        Ok( record.to_owned().try_into()? )
185                    } else {
186                        Err(hdi_extensions::guest_error!(
187                            format!("Entry def mismatch: {:?} != {:?}", Self::app_entry_def(), aed )
188                        ))
189                    }
190                } else {
191                    Err(hdi_extensions::guest_error!(
192                        format!("Action type ({}) does not contain an entry", ActionType::from(record.action()) )
193                    ))
194                }
195            }
196
197            fn to_input(&self) -> $types {
198                $types::$name(self.clone())
199            }
200        }
201    };
202}
203
204
205//
206// HoloHash Extentions
207//
208/// Extend [`AnyLinkableHash`] transformations
209pub trait AnyLinkableHashTransformer : Sized {
210    /// Automatically determine correct type from a string
211    fn try_from_string(input: &str) -> ExternResult<Self>;
212    /// Expect hash type to be an [`ActionHash`] or error
213    fn must_be_action_hash(&self) -> ExternResult<ActionHash>;
214    /// Expect hash type to be an [`EntryHash`] or error
215    fn must_be_entry_hash(&self) -> ExternResult<EntryHash>;
216}
217
218impl AnyLinkableHashTransformer for AnyLinkableHash {
219    fn try_from_string(input: &str) -> ExternResult<Self> {
220        let action_result = ActionHash::try_from( input.to_string() );
221        let entry_result = EntryHash::try_from( input.to_string() );
222        let external_result = ExternalHash::try_from( input.to_string() );
223
224        Ok(
225            match (action_result.is_ok(), entry_result.is_ok(), external_result.is_ok()) {
226                (true, false, false) => action_result.unwrap().into(),
227                (false, true, false) => entry_result.unwrap().into(),
228                (false, false, true) => external_result.unwrap().into(),
229                (false, false, false) => Err(guest_error!(
230                    format!("String '{}' must be an Action or Entry hash", input )
231                ))?,
232                _ => Err(guest_error!(
233                    format!("String '{}' matched multiple hash types; this should not be possible", input )
234                ))?,
235            }
236        )
237    }
238
239    fn must_be_action_hash(&self) -> ExternResult<ActionHash> {
240        match self.to_owned().into_action_hash() {
241            Some(hash) => Ok( hash ),
242            None => Err(guest_error!(
243                format!("Any-linkable hash must be an action hash; not '{}'", self )
244            ))?,
245        }
246    }
247
248    fn must_be_entry_hash(&self) -> ExternResult<EntryHash> {
249        match self.to_owned().into_entry_hash() {
250            Some(hash) => Ok( hash ),
251            None => Err(guest_error!(
252                format!("Any-linkable hash must be an entry hash; not '{}'", self )
253            ))?,
254        }
255    }
256}
257
258/// Extend [`AnyDhtHash`] transformations
259pub trait AnyDhtHashTransformer : Sized {
260    /// Automatically determine correct type from a string
261    fn try_from_string(input: &str) -> ExternResult<Self>;
262}
263
264impl AnyDhtHashTransformer for AnyDhtHash {
265    fn try_from_string(input: &str) -> ExternResult<Self> {
266        let action_result = ActionHash::try_from( input.to_string() );
267        let entry_result = EntryHash::try_from( input.to_string() );
268
269        Ok(
270            match (action_result.is_ok(), entry_result.is_ok()) {
271                (true, false) => action_result.unwrap().into(),
272                (false, true) => entry_result.unwrap().into(),
273                (false, false) => Err(guest_error!(
274                    format!("String '{}' must be an Action or Entry hash", input )
275                ))?,
276                (true, true) => Err(guest_error!(
277                    format!("String '{}' matched Action and Entry hash; this should not be possible", input )
278                ))?,
279            }
280        )
281    }
282}
283
284
285//
286// Advanced "get" Methods
287//
288/// Get and deserialize the given address into the expected app entry struct
289///
290/// **NOTE:** *This will only verify the deserialization of an app entry, it does not validate the
291/// app entry def*
292///
293/// ##### Example: Basic Usage
294/// ```
295/// # use hdi::prelude::*;
296/// # use hdi_extensions::*;
297///
298/// # #[hdk_entry_helper]
299/// # struct PostEntry {
300/// #     pub message: String,
301/// # }
302///
303/// fn test(any_linkable_hash: AnyLinkableHash) -> ExternResult<()> {
304///     let post : PostEntry = summon_app_entry( &any_linkable_hash )?;
305///     Ok(())
306/// }
307/// ```
308pub fn summon_app_entry<T,E>(addr: &AnyLinkableHash) -> ExternResult<T>
309where
310    T: TryFrom<Record, Error = E> + TryFrom<Entry, Error = E>,
311    E: std::fmt::Debug,
312    WasmError: From<E>,
313{
314    match addr.to_owned().into_primitive() {
315        AnyLinkableHashPrimitive::Action(action_hash) => Ok(
316            summon_valid_record( &action_hash )?.try_into()
317                .map_err(|error| convert_deserialize_error( WasmError::from(error) ) )?
318        ),
319        AnyLinkableHashPrimitive::Entry(entry_hash) => Ok(
320            summon_entry( &entry_hash )?.content.try_into()
321                .map_err(|error| convert_deserialize_error( WasmError::from(error) ) )?
322        ),
323        AnyLinkableHashPrimitive::External(external_hash) => Err(guest_error!(
324            format!("Cannot get an entry from any-linkable external hash ({})", external_hash )
325        ))?,
326    }
327}
328
329/// Check that the given address can deserialize to the expected app entry struct
330///
331/// **NOTE:** *This will only verify the deserialization of an app entry, it does not validate the
332/// app entry def*
333///
334/// ##### Example: Basic Usage
335/// ```
336/// # use hdi::prelude::*;
337/// # use hdi_extensions::*;
338///
339/// # #[hdk_entry_helper]
340/// # struct PostEntry {
341/// #     pub message: String,
342/// # }
343///
344/// fn test(any_linkable_hash: AnyLinkableHash) -> ExternResult<()> {
345///     verify_app_entry_struct::<PostEntry>( &any_linkable_hash )?;
346///     Ok(())
347/// }
348/// ```
349pub fn verify_app_entry_struct<T>(addr: &AnyLinkableHash) -> ExternResult<()>
350where
351    T: TryFrom<Record, Error = WasmError> + TryFrom<Entry, Error = WasmError>,
352{
353    let _ : T = summon_app_entry( addr )?;
354
355    Ok(())
356}
357
358/// Get a [`Record`] expecting it to have a specific [`ActionType`]
359pub fn summon_record_type(
360    action_addr: &ActionHash,
361    action_type: &ActionType
362) -> ExternResult<Record> {
363    let record = summon_valid_record( action_addr )?;
364
365    if record.action().action_type() != *action_type {
366        Err(guest_error!(format!("Action address ({}) is not a {} record", action_addr, action_type )))?
367    }
368
369    Ok( record )
370}
371
372macro_rules! get_action_type {
373    ( $action_type:ident, $fn_name:ident ) => {
374        #[doc = concat!("Get an action address expecting it to be a [`Action::", stringify!($action_type), "`]")]
375        pub fn $fn_name(
376            action_addr: &ActionHash,
377        ) -> ExternResult<$action_type> {
378            match summon_record_type( action_addr, &ActionType::$action_type )?.signed_action.hashed.content {
379                Action::$action_type( action_inner ) => Ok( action_inner ),
380                _ => Err(guest_error!("This should be unreachable".to_string())),
381            }
382        }
383    };
384}
385
386get_action_type!( Dna, summon_dna_action );
387get_action_type!( AgentValidationPkg, summon_agent_validation_pkg_action );
388get_action_type!( InitZomesComplete, summon_init_zomes_complete_action );
389get_action_type!( CreateLink, summon_create_link_action );
390get_action_type!( DeleteLink, summon_delete_link_action );
391get_action_type!( OpenChain, summon_open_chain_action );
392get_action_type!( CloseChain, summon_close_chain_action );
393get_action_type!( Create, summon_create_action );
394get_action_type!( Update, summon_update_action );
395get_action_type!( Delete, summon_delete_action );
396
397
398/// Get an action address that is expected to be a [`EntryCreationAction`]
399pub fn summon_creation_action(action_addr: &ActionHash) -> ExternResult<EntryCreationAction> {
400    let create_record = summon_valid_record( action_addr )?;
401    match create_record.signed_action.hashed.content {
402        Action::Create(create) => Ok( create.into() ),
403        Action::Update(update) => Ok( update.into() ),
404        _ => Err(guest_error!(format!("Action address ({}) is not a create action", action_addr ))),
405    }
406}
407
408/// Extend [`Action`] transformations
409pub trait ActionTransformer : Sized {
410    /// Get the app entry that this action points to
411    ///
412    /// ##### Example: Basic Usage
413    /// ```ignore
414    /// use hdi::prelude::*;
415    /// use hdi_extensions::*;
416    ///
417    /// #[hdk_entry_helper]
418    /// struct PostEntry {
419    ///     pub message: String,
420    /// }
421    ///
422    /// #[hdk_entry_defs]
423    /// #[unit_enum(EntryTypesUnit)]
424    /// pub enum EntryTypes {
425    ///     #[entry_def]
426    ///     Post(PostEntry),
427    /// }
428    ///
429    /// fn test(addr: ActionHash) -> ExternResult<()> {
430    ///     let action = summon_action( &addr )?.hashed.content;
431    ///     let entry_type = action.summon_app_entry<EntryTypes>()?;
432    ///     Ok(())
433    /// }
434    /// ```
435    fn summon_app_entry<ET>(&self) -> ExternResult<ET>
436    where
437        ET: EntryTypesHelper,
438        WasmError: From<<ET as EntryTypesHelper>::Error>;
439}
440
441impl ActionTransformer for Action {
442    fn summon_app_entry<ET>(&self) -> ExternResult<ET>
443    where
444        ET: EntryTypesHelper,
445        WasmError: From<<ET as EntryTypesHelper>::Error>,
446    {
447        let action = EntryCreationAction::try_from( self.to_owned() )
448            .map_err(|err| guest_error!(err.0))?;
449        let entry_def = detect_app_entry_def( &action )?;
450        let entry = summon_entry( action.entry_hash() )?.content;
451
452        ET::deserialize_from_type(
453            entry_def.zome_index.clone(),
454            entry_def.entry_index.clone(),
455            &entry,
456        )?.ok_or(guest_error!(
457            format!("No match for entry def ({:?}) in expected entry types", entry_def )
458        ))
459    }
460}
461
462
463//
464// EntryTypesHelper extensions
465//
466/// Detect the [`AppEntryDef`] from a given [`Action`]
467pub fn detect_app_entry_def<A>(action: &A) -> ExternResult<AppEntryDef>
468where
469    A: Into<EntryCreationAction> + Clone,
470{
471    let action : EntryCreationAction = action.to_owned().into();
472    match action.entry_type().to_owned() {
473        EntryType::App(app_entry_def) => Ok( app_entry_def ),
474        entry_type => Err(guest_error!(
475            format!("Expected an app entry type; not {:?}", entry_type )
476        )),
477    }
478}
479
480/// Detect the entry types unit from a given [`Action`]
481pub fn detect_app_entry_unit<ETU,A>(action: &A) -> ExternResult<ETU>
482where
483    ETU: TryFrom<ScopedEntryDefIndex, Error = WasmError>,
484    A: Into<EntryCreationAction> + Clone,
485{
486    let action : EntryCreationAction = action.to_owned().into();
487    let entry_def = detect_app_entry_def( &action )?;
488    ETU::try_from(ScopedEntryDefIndex {
489        zome_index: entry_def.zome_index,
490        zome_type: entry_def.entry_index,
491    })
492}
493
494
495//
496// Standard Inputs
497//
498/// Input for getting links based on direction (ignoring type/tag)
499#[derive(Clone, Serialize, Deserialize, Debug)]
500pub struct LinkDirectionInput {
501    pub base: AnyLinkableHash,
502    pub target: AnyLinkableHash,
503}