diff --git a/src/cli.rs b/src/cli.rs index c1a37deb7..6403cbc1f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -52,17 +52,15 @@ impl HcScaffold { let template_type = self.get_template_type(¤t_dir, scaffold_config.as_ref())?; match self.command { - HcScaffoldCommand::WebApp(web_app) => web_app.run(&template_type).await?, - HcScaffoldCommand::Template(template) => template.run(&template_type)?, - HcScaffoldCommand::Dna(dna) => dna.run(&template_type)?, - HcScaffoldCommand::Zome(zome) => zome.run(&template_type)?, - HcScaffoldCommand::EntryType(entry_type) => entry_type.run(&template_type)?, - HcScaffoldCommand::LinkType(link_type) => link_type.run(&template_type)?, - HcScaffoldCommand::Collection(collection) => collection.run(&template_type)?, - HcScaffoldCommand::Example(example) => example.run(&template_type).await?, + HcScaffoldCommand::WebApp(web_app) => web_app.run(&template_type).await, + HcScaffoldCommand::Template(template) => template.run(&template_type), + HcScaffoldCommand::Dna(dna) => dna.run(&template_type), + HcScaffoldCommand::Zome(zome) => zome.run(&template_type), + HcScaffoldCommand::EntryType(entry_type) => entry_type.run(&template_type), + HcScaffoldCommand::LinkType(link_type) => link_type.run(&template_type), + HcScaffoldCommand::Collection(collection) => collection.run(&template_type), + HcScaffoldCommand::Example(example) => example.run(&template_type).await, } - - Ok(()) } fn get_template_type( diff --git a/src/error.rs b/src/error.rs index 2d3a45b8a..a6ab7c4d0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -54,8 +54,8 @@ pub enum ScaffoldError { #[error("Folder already exists: {0}")] FolderAlreadyExists(PathBuf), - #[error("Invalid reserved word: {0}")] - InvalidReservedWord(String), + #[error("Invalid use of reserved {context} keyword '{word}' in this context.")] + InvalidReservedWord { context: String, word: String }, #[error("Invalid path {0}: {1}")] InvalidPath(PathBuf, String), diff --git a/src/reserved_words.rs b/src/reserved_words.rs index 6a31b5fe2..0bbcf322e 100644 --- a/src/reserved_words.rs +++ b/src/reserved_words.rs @@ -1,44 +1,120 @@ +use std::collections::HashSet; + use crate::error::{ScaffoldError, ScaffoldResult}; -const RESERVED_WORDS: [&str; 27] = [ - "type", +/// Returns an error if the given string is invalid due to it being a reserved word +pub fn check_for_reserved_keywords(string_to_check: &str) -> ScaffoldResult<()> { + let reserved_holochain_words_set = HashSet::from(HOLOCHAIN_RESERVED_KEYWORDS); + let reserved_rust_words_set = HashSet::from(RUST_RESERVED_KEYWORDS); + let reserved_javascript_words_set = HashSet::from(JAVASCRIPT_RESERVED_KEYWORDS); + + if reserved_holochain_words_set.contains(string_to_check.to_ascii_lowercase().as_str()) { + return Err(ScaffoldError::InvalidReservedWord { + context: "holochain".to_string(), + word: string_to_check.to_string(), + }); + } + + if reserved_rust_words_set.contains(string_to_check.to_ascii_lowercase().as_str()) { + return Err(ScaffoldError::InvalidReservedWord { + context: "rust".to_string(), + word: string_to_check.to_string(), + }); + } + + if reserved_javascript_words_set.contains(string_to_check.to_ascii_lowercase().as_str()) { + return Err(ScaffoldError::InvalidReservedWord { + context: "javascript".to_string(), + word: string_to_check.to_string(), + }); + } + + Ok(()) +} + +const HOLOCHAIN_RESERVED_KEYWORDS: [&str; 16] = [ "role", - "enum", - "pub", - "fn", - "mod", - "struct", - "const", - "Option", - "Result", - "crate", "hdi", "hdk", - "return", - "if", + "action", + "entry", + "record", + "zome", + "dna", + "entrytype", + "entryhash", + "actionhash", + "agentpubkey", + "anylinkablehash", + "holohash", + "externalhash", + "call", +]; + +// +const RUST_RESERVED_KEYWORDS: [&str; 50] = [ + "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", + "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", + "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", + "while", "abstract", "async", "await", "become", "box", "do", "final", "macro", "override", + "priv", "try", "typeof", "unsized", "virtual", "yield", +]; + +// +const JAVASCRIPT_RESERVED_KEYWORDS: [&str; 35] = [ + "break", + "case", + "catch", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", "else", - "match", - "Action", - "Entry", - "Record", - "Zome", - "Dna", - "EntryType", - "EntryHash", - "ActionHash", - "AgentPubKey", - "Call", + "export", + "extends", + "false", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "new", + "null", + "return", + "super", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "with", ]; -/// Returns an error if the given string is invalid due to it being a reserved word -pub fn check_for_reserved_words(string_to_check: &str) -> ScaffoldResult<()> { - if RESERVED_WORDS - .iter() - .any(|w| string_to_check.eq_ignore_ascii_case(w)) - { - return Err(ScaffoldError::InvalidReservedWord( - string_to_check.to_string(), - )); +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_for_reserved_keywords_works() { + let valid = check_for_reserved_keywords("Value"); + assert!(valid.is_ok()); + + let invalid = check_for_reserved_keywords("static"); + assert!(invalid.is_err()); + + let invalid = check_for_reserved_keywords("EntryType"); + assert!(invalid.is_err()); + + let invalid = check_for_reserved_keywords("new"); + assert!(invalid.is_err()); } - Ok(()) } diff --git a/src/scaffold/collection.rs b/src/scaffold/collection.rs index ad86b6387..15f146ab9 100644 --- a/src/scaffold/collection.rs +++ b/src/scaffold/collection.rs @@ -7,7 +7,7 @@ use serde::Serialize; use crate::{ error::{ScaffoldError, ScaffoldResult}, file_tree::FileTree, - reserved_words::check_for_reserved_words, + reserved_words::check_for_reserved_keywords, templates::{collection::scaffold_collection_templates, ScaffoldedTemplate}, }; @@ -74,7 +74,7 @@ pub fn scaffold_collection( no_ui: bool, no_spec: bool, ) -> ScaffoldResult { - check_for_reserved_words(collection_name)?; + check_for_reserved_keywords(collection_name)?; let all_entries = get_all_entry_types(&integrity_zome_file_tree)?.ok_or( ScaffoldError::NoEntryTypesDefFoundForIntegrityZome( diff --git a/src/scaffold/dna.rs b/src/scaffold/dna.rs index 943a5b65e..c6282cb16 100644 --- a/src/scaffold/dna.rs +++ b/src/scaffold/dna.rs @@ -6,7 +6,7 @@ use crate::{ dir_exists, file_content, find_files_by_name, insert_file, insert_file_tree_in_dir, FileTree, }, - reserved_words::check_for_reserved_words, + reserved_words::check_for_reserved_keywords, templates::{dna::scaffold_dna_templates, ScaffoldedTemplate}, utils::choose_directory_path, }; @@ -168,7 +168,7 @@ pub fn scaffold_dna( template_file_tree: &FileTree, dna_name: &str, ) -> ScaffoldResult { - check_for_reserved_words(dna_name)?; + check_for_reserved_keywords(dna_name)?; let new_dna_file_tree: FileTree = dir! { "zomes" => dir! { diff --git a/src/scaffold/entry_type.rs b/src/scaffold/entry_type.rs index 604bb2967..d3b0eb6a1 100644 --- a/src/scaffold/entry_type.rs +++ b/src/scaffold/entry_type.rs @@ -2,7 +2,7 @@ use std::{ffi::OsString, path::PathBuf}; use crate::{ file_tree::FileTree, - reserved_words::check_for_reserved_words, + reserved_words::check_for_reserved_keywords, templates::{entry_type::scaffold_entry_type_templates, ScaffoldedTemplate}, }; @@ -47,7 +47,7 @@ pub fn scaffold_entry_type( no_ui: bool, no_spec: bool, ) -> ScaffoldResult { - check_for_reserved_words(name)?; + check_for_reserved_keywords(name)?; if no_ui { let warning_text = r#" @@ -139,13 +139,13 @@ inadvertently reference or expect elements from the skipped entry type."# let coordinator_zome = match coordinator_zomes_for_integrity.len() { 0 => Err(ScaffoldError::NoCoordinatorZomesFoundForIntegrityZome( zome_file_tree.dna_file_tree.dna_manifest.name(), - zome_file_tree.zome_manifest.name.0.to_string(), + zome_file_tree.zome_manifest.name.to_string(), )), 1 => Ok(coordinator_zomes_for_integrity[0].clone()), _ => { let names: Vec = coordinator_zomes_for_integrity .iter() - .map(|z| z.name.0.to_string()) + .map(|z| z.name.to_string()) .collect(); let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Which coordinator zome should the CRUD functions be scaffolded in?") @@ -217,7 +217,7 @@ fn check_field_definitions( .iter() .filter_map(|f| f.linked_from.clone()) .filter_map(|t| match t { - Referenceable::Agent { .. } => None, + Referenceable::Agent { .. } | Referenceable::ExternalHash { .. } => None, Referenceable::EntryType(et) => Some(et), }) .collect(); diff --git a/src/scaffold/entry_type/coordinator.rs b/src/scaffold/entry_type/coordinator.rs index db3b330bd..c7e0bdfed 100644 --- a/src/scaffold/entry_type/coordinator.rs +++ b/src/scaffold/entry_type/coordinator.rs @@ -145,10 +145,10 @@ pub fn add_crud_functions_to_coordinator( } fn no_update_read_handler(entry_def: &EntryDefinition) -> TokenStream { - let hash_type = entry_def.referenceable().hash_type().to_string(); + let hash_type = entry_def.referenceable().field_type().to_string(); let snake_entry_def_name = entry_def.name.to_case(Case::Snake); - match entry_def.referenceable().hash_type() { + match entry_def.referenceable().field_type() { FieldType::ActionHash => { let get_entry_def_function_name = format_ident!("get_{snake_entry_def_name}"); let entry_hash_param = format_ident!("{snake_entry_def_name}_hash"); diff --git a/src/scaffold/entry_type/definitions.rs b/src/scaffold/entry_type/definitions.rs index 281a64357..a0eaeab6f 100644 --- a/src/scaffold/entry_type/definitions.rs +++ b/src/scaffold/entry_type/definitions.rs @@ -1,13 +1,14 @@ use std::str::FromStr; +use anyhow::Context; use convert_case::{Case, Casing}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; -use crate::{error::ScaffoldError, reserved_words::check_for_reserved_words, utils::check_case}; +use crate::{error::ScaffoldError, reserved_words::check_for_reserved_keywords, utils::check_case}; -#[derive(Deserialize, Debug, Clone, Serialize)] +#[derive(Deserialize, Debug, Clone, Serialize, Eq, PartialEq)] #[serde(tag = "type")] pub enum FieldType { #[serde(rename = "bool")] @@ -24,6 +25,7 @@ pub enum FieldType { ActionHash, EntryHash, DnaHash, + ExternalHash, Enum { label: String, variants: Vec, @@ -64,6 +66,7 @@ impl std::fmt::Display for FieldType { FieldType::ActionHash => "ActionHash", FieldType::EntryHash => "EntryHash", FieldType::DnaHash => "DnaHash", + FieldType::ExternalHash => "ExternalHash", FieldType::AgentPubKey => "AgentPubKey", FieldType::Enum { .. } => "Enum", }; @@ -83,6 +86,7 @@ impl FieldType { FieldType::ActionHash, FieldType::EntryHash, FieldType::DnaHash, + FieldType::ExternalHash, FieldType::AgentPubKey, FieldType::Enum { label: String::new(), @@ -104,6 +108,7 @@ impl FieldType { ActionHash => quote!(ActionHash), DnaHash => quote!(DnaHash), EntryHash => quote!(EntryHash), + ExternalHash => quote!(ExternalHash), AgentPubKey => quote!(AgentPubKey), Enum { label, .. } => { let ident = format_ident!("{}", label); @@ -126,6 +131,7 @@ impl FieldType { ActionHash => "ActionHash", EntryHash => "EntryHash", DnaHash => "DnaHash", + ExternalHash => "ExternalHash", Enum { label, .. } => label, } } @@ -186,7 +192,7 @@ impl FieldDefinition { cardinality: Cardinality, linked_from: Option, ) -> Result { - check_for_reserved_words(&field_name)?; + check_for_reserved_keywords(&field_name)?; Ok(FieldDefinition { field_name, field_type, @@ -222,7 +228,7 @@ pub struct EntryTypeReference { } impl EntryTypeReference { - pub fn hash_type(&self) -> FieldType { + pub fn field_type(&self) -> FieldType { if self.reference_entry_hash { FieldType::EntryHash } else { @@ -240,7 +246,7 @@ impl EntryTypeReference { } } - pub fn to_string(&self, c: &Cardinality) -> String { + pub fn name_by_cardinality(&self, c: &Cardinality) -> String { match c { Cardinality::Vector => pluralizer::pluralize(self.entry_type.as_str(), 2, false), _ => pluralizer::pluralize(self.entry_type.as_str(), 1, false), @@ -273,10 +279,11 @@ impl FromStr for EntryTypeReference { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, PartialOrd)] pub enum Referenceable { Agent { role: String }, EntryType(EntryTypeReference), + ExternalHash { name: String }, } impl Serialize for Referenceable { @@ -286,7 +293,7 @@ impl Serialize for Referenceable { { let mut state = serializer.serialize_struct("Referenceable", 3)?; state.serialize_field("name", &self.to_string(&Cardinality::Single))?; - state.serialize_field("hash_type", &self.hash_type().to_string())?; + state.serialize_field("hash_type", &self.field_type().to_string())?; state.serialize_field("singular_arg", &self.field_name(&Cardinality::Single))?; state.end() } @@ -296,29 +303,37 @@ impl FromStr for Referenceable { type Err = ScaffoldError; fn from_str(s: &str) -> Result { - let sp: Vec<&str> = s.split(':').collect(); + let parts: Vec<&str> = s.split(':').collect(); + let type_name = parts + .first() + .context(format!("The first argument in '{}' is invalid", s))?; - check_case(sp[0], "referenceable", Case::Snake)?; + check_case(type_name, "referenceable", Case::Snake)?; - Ok(match sp[0] { - "agent" => match sp.len() { - 0 | 1 => Referenceable::Agent { - role: String::from("agent"), - }, - _ => Referenceable::Agent { - role: sp[1].to_string(), - }, - }, - _ => Referenceable::EntryType(EntryTypeReference::from_str(s)?), - }) + match *type_name { + "agent" => { + let role = parts.get(1).unwrap_or(&"agent").to_string(); + Ok(Referenceable::Agent { role }) + } + _ => { + if parts.get(1) == Some(&"ExternalHash") { + Ok(Referenceable::ExternalHash { + name: type_name.to_string(), + }) + } else { + EntryTypeReference::from_str(s).map(Referenceable::EntryType) + } + } + } } } impl Referenceable { - pub fn hash_type(&self) -> FieldType { + pub fn field_type(&self) -> FieldType { match self { Referenceable::Agent { .. } => FieldType::AgentPubKey, - Referenceable::EntryType(r) => r.hash_type(), + Referenceable::EntryType(r) => r.field_type(), + Referenceable::ExternalHash { .. } => FieldType::ExternalHash, } } @@ -326,7 +341,7 @@ impl Referenceable { let s = self.to_string(c).to_case(Case::Snake); match self { - Referenceable::Agent { .. } => s, + Referenceable::Agent { .. } | Referenceable::ExternalHash { .. } => s, Referenceable::EntryType(e) => e.field_name(c), } } @@ -335,6 +350,7 @@ impl Referenceable { let singular = match self { Referenceable::Agent { role } => role.clone(), Referenceable::EntryType(r) => r.entry_type.clone(), + Referenceable::ExternalHash { name } => name.clone(), }; match c { diff --git a/src/scaffold/entry_type/utils.rs b/src/scaffold/entry_type/utils.rs index fccbf9c12..d3c4bdb44 100644 --- a/src/scaffold/entry_type/utils.rs +++ b/src/scaffold/entry_type/utils.rs @@ -1,167 +1,170 @@ +use anyhow::Context; use convert_case::Case; use dialoguer::{theme::ColorfulTheme, Select}; +use super::definitions::{EntryTypeReference, Referenceable}; use crate::{ error::{ScaffoldError, ScaffoldResult}, - reserved_words::check_for_reserved_words, + reserved_words::check_for_reserved_keywords, scaffold::zome::ZomeFileTree, utils::input_with_case, }; -use super::{ - definitions::{EntryTypeReference, Referenceable}, - integrity::get_all_entry_types, -}; - pub fn choose_reference_entry_hash(prompt: &str, recommended: bool) -> ScaffoldResult { - let selection = if recommended { - Select::with_theme(&ColorfulTheme::default()) - .with_prompt(prompt) - .default(0) - .item("EntryHash (recommended)") - .item("ActionHash") - .interact()? + let options = if recommended { + [("EntryHash", true), ("ActionHash", false)] } else { - Select::with_theme(&ColorfulTheme::default()) - .with_prompt(prompt) - .default(0) - .item("ActionHash (recommended)") - .item("EntryHash") - .interact()? + [("ActionHash", false), ("EntryHash", true)] }; - if recommended { - Ok(selection == 0) - } else { - Ok(selection != 0) - } -} - -fn inner_choose_referenceable( - all_entries: &[EntryTypeReference], - prompt: &str, - optional: bool, -) -> ScaffoldResult> { - let mut all_options: Vec = all_entries - .iter() - .map(|r| r.entry_type.to_owned()) - .collect(); - - all_options.push("Agent".to_string()); - - if optional { - all_options.push("[None]".to_string()); - } - let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt(prompt) .default(0) - .items(&all_options[..]) + .items(&options.map(|(name, _)| name)) .interact()?; - if selection == all_entries.len() { - let role = input_with_case(&String::from( - "Which role does this agent play in the relationship ? (eg. \"creator\", \"invitee\")", - ), Case::Snake)?; - check_for_reserved_words(&role)?; - Ok(Some(Referenceable::Agent { role })) - } else if selection == all_entries.len() + 1 { - Ok(None) - } else { - Ok(Some(Referenceable::EntryType(EntryTypeReference { - entry_type: all_options[selection].clone(), - reference_entry_hash: choose_reference_entry_hash( - &String::from("Reference this entry type with its entry hash or its action hash?"), - all_entries[selection].reference_entry_hash, - )?, - }))) - } -} - -pub fn choose_referenceable( - all_entries: &[EntryTypeReference], - prompt: &str, -) -> ScaffoldResult { - let maybe_reference_type = inner_choose_referenceable(all_entries, prompt, false)?; - - Ok(maybe_reference_type.expect("reference type should not be None")) -} + let (_, value) = options[selection]; -pub fn choose_optional_referenceable( - all_entries: &[EntryTypeReference], - prompt: &str, -) -> ScaffoldResult> { - inner_choose_referenceable(all_entries, prompt, true) -} - -pub fn choose_entry_type_reference( - all_entries: &[EntryTypeReference], - prompt: &str, -) -> ScaffoldResult { - let all_options: Vec = all_entries.iter().cloned().map(|r| r.entry_type).collect(); - - let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt(prompt) - .default(0) - .items(&all_options[..]) - .interact()?; - - Ok(all_entries[selection].clone()) + Ok(value) } pub fn get_or_choose_referenceable( + prompt: &str, zome_file_tree: &ZomeFileTree, entry_type: Option<&Referenceable>, - prompt: &str, + all_entries: &[EntryTypeReference], ) -> ScaffoldResult { - let all_entries = get_all_entry_types(zome_file_tree)?.unwrap_or_else(Vec::new); - match &entry_type { - None => choose_referenceable(&all_entries, prompt), Some(Referenceable::Agent { role }) => { - check_for_reserved_words(role)?; + check_for_reserved_keywords(role)?; Ok(Referenceable::Agent { role: role.clone() }) } Some(Referenceable::EntryType(app_entry_reference)) => { - let all_entries: Vec = all_entries.into_iter().map(|e| e.entry_type).collect(); + let all_entries: Vec<&str> = + all_entries.iter().map(|e| e.entry_type.as_str()).collect(); all_entries .into_iter() - .find(|et| et.eq(&app_entry_reference.entry_type.to_string())) + .find(|et| et == &app_entry_reference.entry_type) .ok_or(ScaffoldError::EntryTypeNotFound( app_entry_reference.entry_type.to_string().clone(), zome_file_tree.dna_file_tree.dna_manifest.name(), - zome_file_tree.zome_manifest.name.0.to_string(), + zome_file_tree.zome_manifest.name.to_string(), ))?; Ok(Referenceable::EntryType(app_entry_reference.clone())) } + _ => choose_referenceable(all_entries, prompt), } } pub fn get_or_choose_optional_reference_type( + prompt: &str, zome_file_tree: &ZomeFileTree, entry_type: Option<&Referenceable>, - prompt: &str, + all_entries: &[EntryTypeReference], ) -> ScaffoldResult> { - let all_entries = get_all_entry_types(zome_file_tree)?.unwrap_or_else(Vec::new); - match entry_type { - None => choose_optional_referenceable(&all_entries, prompt), Some(Referenceable::Agent { .. }) => Ok(entry_type.cloned()), Some(Referenceable::EntryType(app_entry_reference)) => { - let all_entries: Vec = all_entries.into_iter().map(|e| e.entry_type).collect(); + let all_entries: Vec<&str> = + all_entries.iter().map(|e| e.entry_type.as_str()).collect(); all_entries .into_iter() - .find(|et| et.eq(&app_entry_reference.entry_type.to_string())) + .find(|et| et == &app_entry_reference.entry_type) .ok_or(ScaffoldError::EntryTypeNotFound( app_entry_reference.entry_type.to_string().clone(), zome_file_tree.dna_file_tree.dna_manifest.name(), - zome_file_tree.zome_manifest.name.0.to_string(), + zome_file_tree.zome_manifest.name.to_string(), ))?; Ok(entry_type.cloned()) } + _ => choose_optional_referenceable(all_entries, prompt), + } +} + +pub fn choose_referenceable( + all_entries: &[EntryTypeReference], + prompt: &str, +) -> ScaffoldResult { + let maybe_reference_type = inner_choose_referenceable(all_entries, prompt, None)?; + Ok(maybe_reference_type.context("Reference type should not be None")?) +} + +pub fn choose_optional_referenceable( + all_entries: &[EntryTypeReference], + prompt: &str, +) -> ScaffoldResult> { + inner_choose_referenceable( + all_entries, + prompt, + Some(vec!["[None] (Use this link to attach meta-data only)"]), + ) +} + +fn inner_choose_referenceable( + all_entries: &[EntryTypeReference], + prompt: &str, + extra_options: Option>, +) -> ScaffoldResult> { + let mut all_options: Vec = all_entries + .iter() + .map(|r| r.entry_type.to_owned()) + .collect(); + + all_options.push("ExternalHash".to_string()); + all_options.push("Agent".to_string()); + + if let Some(options) = extra_options { + all_options.extend(options.into_iter().map(String::from).collect::>()) + } + + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .default(0) + .items(&all_options[..]) + .interact()?; + + match all_options[selection].as_str() { + "Agent" => { + let role = input_with_case( + "Which role does this agent play in the relationship ? (eg. \"creator\", \"invitee\")", + Case::Snake, + )?; + check_for_reserved_keywords(&role)?; + Ok(Some(Referenceable::Agent { role })) + } + "ExternalHash" => { + let name = input_with_case( + "What name should be given to the link for this hash?", + Case::Snake, + )?; + Ok(Some(Referenceable::ExternalHash { name })) + } + entry_type if entry_type.starts_with("[None]") => Ok(None), + entry_type => Ok(Some(Referenceable::EntryType(EntryTypeReference { + entry_type: entry_type.to_owned(), + reference_entry_hash: choose_reference_entry_hash( + "Reference this entry type with its entry hash or its action hash?", + all_entries[selection].reference_entry_hash, + )?, + }))), } } + +pub fn choose_entry_type_reference( + all_entries: &[EntryTypeReference], + prompt: &str, +) -> ScaffoldResult { + let all_options: Vec = all_entries.iter().cloned().map(|r| r.entry_type).collect(); + + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .default(0) + .items(&all_options[..]) + .interact()?; + + Ok(all_entries[selection].clone()) +} diff --git a/src/scaffold/link_type.rs b/src/scaffold/link_type.rs index 087bfc956..d06f59bf8 100644 --- a/src/scaffold/link_type.rs +++ b/src/scaffold/link_type.rs @@ -19,6 +19,7 @@ use super::{ dna::DnaFileTree, entry_type::{ definitions::{Cardinality, Referenceable}, + integrity::get_all_entry_types, utils::{get_or_choose_optional_reference_type, get_or_choose_referenceable}, }, zome::{utils::get_coordinator_zomes_for_integrity, ZomeFileTree}, @@ -40,31 +41,35 @@ pub fn scaffold_link_type( ) -> ScaffoldResult { let dna_manifest_path = zome_file_tree.dna_file_tree.dna_manifest_path.clone(); let zome_manifest = zome_file_tree.zome_manifest.clone(); + let all_entry_types = get_all_entry_types(&zome_file_tree)?.unwrap_or_default(); let from_referenceable = get_or_choose_referenceable( + "Link from which entry type?", &zome_file_tree, from_referenceable, - "Link from which entry type?", + &all_entry_types, )?; let to_referenceable = get_or_choose_optional_reference_type( + "Link to which entry type?", &zome_file_tree, to_referenceable, - "Link to which entry type?", + &all_entry_types, )?; - let link_type = match to_referenceable.clone() { - Some(to_referenceable) => link_type_name(&from_referenceable, &to_referenceable), + let link_type = match &to_referenceable { + Some(to_referenceable) => link_type_name(&from_referenceable, to_referenceable), None => input_with_case("Enter link type name:", Case::Pascal)?, }; let bidirectional = match (&to_referenceable, bidirectional) { (None, _) => false, (_, Some(b)) => b, - (_, None) => Confirm::with_theme(&ColorfulTheme::default()) + _ => Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Should the link be bidirectional?") .interact()?, }; + let delete = match delete { Some(d) => d, None => Confirm::with_theme(&ColorfulTheme::default()) @@ -142,13 +147,13 @@ pub use {}::*; let coordinator_zome = match coordinator_zomes_for_integrity.len() { 0 => Err(ScaffoldError::NoCoordinatorZomesFoundForIntegrityZome( zome_file_tree.dna_file_tree.dna_manifest.name(), - zome_file_tree.zome_manifest.name.0.to_string(), + zome_file_tree.zome_manifest.name.to_string(), )), 1 => Ok(coordinator_zomes_for_integrity[0].clone()), _ => { let names: Vec = coordinator_zomes_for_integrity .iter() - .map(|z| z.name.0.to_string()) + .map(|z| z.name.to_string()) .collect(); let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt( diff --git a/src/scaffold/link_type/coordinator.rs b/src/scaffold/link_type/coordinator.rs index 27b3937c6..600b0b94b 100644 --- a/src/scaffold/link_type/coordinator.rs +++ b/src/scaffold/link_type/coordinator.rs @@ -31,7 +31,6 @@ pub fn add_link_type_functions_to_coordinator( let zome_manifest = coordinator_zome_file_tree.zome_manifest.clone(); let snake_link_type_name = link_type_name.to_case(Case::Snake); - let new_file_path = coordinator_zome_file_tree .zome_crate_path .join("src") @@ -41,7 +40,7 @@ pub fn add_link_type_functions_to_coordinator( let lib_rs_path = crate_src_path.join("lib.rs"); let mut file_tree = coordinator_zome_file_tree.dna_file_tree.file_tree(); - let handlers = match to_referenceable { + let link_type_handlers_file = match to_referenceable { Some(r) => normal_handlers( integrity_zome_name, from_referenceable, @@ -52,16 +51,14 @@ pub fn add_link_type_functions_to_coordinator( None => metadata_handlers(integrity_zome_name, link_type_name, from_referenceable), }; - let file = unparse_pretty(&syn::parse_quote! { #handlers }); + let file = unparse_pretty(&syn::parse_quote! { #link_type_handlers_file }); + // insert handlers file insert_file(&mut file_tree, &new_file_path, &file)?; - // 2. Add this file as a module in the entry point for the crate + // add newly created file to lib.rs file map_file(&mut file_tree, &lib_rs_path, |contents| { - Ok(format!( - r#"pub mod {snake_link_type_name}; -{contents}"#, - )) + Ok(format!("pub mod {snake_link_type_name};\n{contents}",)) })?; let dna_file_tree = DnaFileTree::from_dna_manifest_path(file_tree, &dna_manifest_path)?; @@ -70,6 +67,39 @@ pub fn add_link_type_functions_to_coordinator( Ok(zome_file_tree) } +fn normal_handlers( + integrity_zome_name: &str, + from_referenceable: &Referenceable, + to_referenceable: &Referenceable, + delete: bool, + bidirectional: bool, +) -> TokenStream { + let inverse_get_handler = bidirectional + .then(|| get_links_handler(to_referenceable, from_referenceable, delete)) + .unwrap_or_default(); + + let delete_link_handler = delete + .then(|| remove_link_handlers(from_referenceable, to_referenceable, bidirectional)) + .unwrap_or_default(); + + let integrity_zome_name = format_ident!("{integrity_zome_name}"); + let add_links_handler = add_link_handler(from_referenceable, to_referenceable, bidirectional); + let get_links_handler = get_links_handler(from_referenceable, to_referenceable, delete); + + quote! { + use hdk::prelude::*; + use #integrity_zome_name::*; + + #add_links_handler + + #get_links_handler + + #inverse_get_handler + + #delete_link_handler + } +} + fn metadata_handlers( integrity_zome_name: &str, link_type_name: &str, @@ -82,7 +112,7 @@ fn metadata_handlers( .field_name(&Cardinality::Single) .to_case(Case::Snake) ); - let from_arg_type = format_ident!("{}", from_referenceable.hash_type().to_string()); + let from_field_type = format_ident!("{}", from_referenceable.field_type().to_string()); let snake_from = format_ident!( "{}", from_referenceable @@ -97,9 +127,9 @@ fn metadata_handlers( ); let pascal_link_type_name = format_ident!("{}", link_type_name.to_case(Case::Pascal)); let snake_link_type_name = format_ident!("{}", link_type_name.to_case(Case::Snake)); - let add_link_type_struct_name = + let create_link_input_struct_name = format_ident!("Add{pascal_link_type_name}For{pascal_from}Input"); - let add_link_type_function_name = format_ident!("add_{snake_link_type_name}_for_{snake_from}"); + let create_link_function_name = format_ident!("add_{snake_link_type_name}_for_{snake_from}"); let plural_snake_link_type_name = format_ident!( "{}", pluralizer::pluralize(&link_type_name.to_case(Case::Snake), 2, false) @@ -108,40 +138,40 @@ fn metadata_handlers( format_ident!("get_{plural_snake_link_type_name}_for_{snake_from}"); quote! { - use hdk::prelude::*; - use #integrity_zome_name::*; - - #[derive(Serialize, Deserialize, Debug)] - pub struct #add_link_type_struct_name { - pub #snake_from_arg: #from_arg_type, - pub #snake_link_type_name: String, - } - - #[hdk_extern] - pub fn #add_link_type_function_name(input: #add_link_type_struct_name) -> ExternResult<()> { - create_link( - input.#snake_from_arg.clone(), - input.#snake_from_arg, - LinkTypes::#pascal_link_type_name, - input.#snake_link_type_name, - )?; - Ok(()) - } - - #[hdk_extern] - pub fn #get_link_type_function_name(#snake_from_arg: #from_arg_type) -> ExternResult> { - let links = get_links( - GetLinksInputBuilder::try_new(#snake_from_arg, LinkTypes::#pascal_link_type_name)?.build(), - )?; - let #snake_link_type_name = links - .into_iter() - .map(|link| - String::from_utf8(link.tag.into_inner()) - .map_err(|e| wasm_error!(WasmErrorInner::Guest(format!("Error converting link tag to string: {:?}", e)))) - ) - .collect::>>()?; - Ok(#snake_link_type_name) - } + use hdk::prelude::*; + use #integrity_zome_name::*; + + #[derive(Serialize, Deserialize, Debug)] + pub struct #create_link_input_struct_name { + pub #snake_from_arg: #from_field_type, + pub #snake_link_type_name: String, + } + + #[hdk_extern] + pub fn #create_link_function_name(input: #create_link_input_struct_name) -> ExternResult<()> { + create_link( + input.#snake_from_arg.clone(), + input.#snake_from_arg, + LinkTypes::#pascal_link_type_name, + input.#snake_link_type_name, + )?; + Ok(()) + } + + #[hdk_extern] + pub fn #get_link_type_function_name(#snake_from_arg: #from_field_type) -> ExternResult> { + let links = get_links( + GetLinksInputBuilder::try_new(#snake_from_arg, LinkTypes::#pascal_link_type_name)?.build(), + )?; + let #snake_link_type_name = links + .into_iter() + .map(|link| + String::from_utf8(link.tag.into_inner()) + .map_err(|e| wasm_error!(WasmErrorInner::Guest(format!("Error converting link tag to string: {:?}", e)))) + ) + .collect::>>()?; + Ok(#snake_link_type_name) + } } } @@ -150,8 +180,8 @@ pub fn add_link_handler( to_referenceable: &Referenceable, bidirectional: bool, ) -> TokenStream { - let from_hash_type = format_ident!("{}", from_referenceable.hash_type().to_string()); - let to_hash_type = format_ident!("{}", to_referenceable.hash_type().to_string()); + let from_field_type = format_ident!("{}", from_referenceable.field_type().to_string()); + let to_field_type = format_ident!("{}", to_referenceable.field_type().to_string()); let target_field_name = format_ident!( "target_{}", to_referenceable.field_name(&Cardinality::Single) @@ -200,8 +230,8 @@ pub fn add_link_handler( quote! { #[derive(Serialize, Deserialize, Debug)] pub struct #add_link_input_struct_name { - pub #base_field_name: #from_hash_type, - pub #target_field_name: #to_hash_type, + pub #base_field_name: #from_field_type, + pub #target_field_name: #to_field_type, } #[hdk_extern] @@ -225,18 +255,21 @@ pub fn get_links_handler( ) -> TokenStream { match to_referenceable { Referenceable::Agent { .. } => { - get_links_handler_to_agent(from_referenceable, to_referenceable, delete) + get_links_to_agent_handler(from_referenceable, to_referenceable, delete) + } + Referenceable::ExternalHash { .. } => { + get_links_to_any_linkable_hash_handler(from_referenceable, to_referenceable, delete) } - Referenceable::EntryType(e) => get_links_handler_to_entry(from_referenceable, e, delete), + Referenceable::EntryType(e) => get_links_to_entry_handler(from_referenceable, e, delete), } } -fn get_links_handler_to_agent( +fn get_links_to_agent_handler( from_referenceable: &Referenceable, to_referenceable: &Referenceable, delete: bool, ) -> TokenStream { - let from_hash_type = format_ident!("{}", from_referenceable.hash_type().to_string()); + let from_field_type = format_ident!("{}", from_referenceable.field_type().to_string()); let from_arg_name = format_ident!("{}", from_referenceable.field_name(&Cardinality::Single)); let pascal_link_type_name = @@ -263,7 +296,7 @@ fn get_links_handler_to_agent( quote::quote! { #[hdk_extern] pub fn #get_deleted_entry_for_entry_function_name( - #from_arg_name: #from_hash_type, + #from_arg_name: #from_field_type, ) -> ExternResult)>> { let details = get_link_details( #from_arg_name, @@ -286,7 +319,7 @@ fn get_links_handler_to_agent( quote::quote! { #[hdk_extern] - pub fn #get_entry_for_entry_function_name(#from_arg_name: #from_hash_type) -> ExternResult> { + pub fn #get_entry_for_entry_function_name(#from_arg_name: #from_field_type) -> ExternResult> { get_links( GetLinksInputBuilder::try_new(#from_arg_name, LinkTypes::#pascal_link_type_name)?.build(), ) @@ -296,12 +329,12 @@ fn get_links_handler_to_agent( } } -fn get_links_handler_to_entry( +fn get_links_to_entry_handler( from_referenceable: &Referenceable, to_entry_type: &EntryTypeReference, delete: bool, ) -> TokenStream { - let from_hash_type = format_ident!("{}", from_referenceable.hash_type().to_string()); + let from_field_type = format_ident!("{}", from_referenceable.field_type().to_string()); let from_arg_name = format_ident!("{}", from_referenceable.field_name(&Cardinality::Single)); let pascal_link_type_name = format_ident!( @@ -320,7 +353,7 @@ fn get_links_handler_to_entry( let plural_snake_to_entry_type = format_ident!( "{}", to_entry_type - .to_string(&Cardinality::Vector) + .name_by_cardinality(&Cardinality::Vector) .to_case(Case::Snake) ); @@ -333,7 +366,7 @@ fn get_links_handler_to_entry( quote::quote! { #[hdk_extern] pub fn #get_deleted_entry_for_entry_function_name( - #from_arg_name: #from_hash_type, + #from_arg_name: #from_field_type, ) -> ExternResult)>> { let details = get_link_details( #from_arg_name, @@ -356,7 +389,7 @@ fn get_links_handler_to_entry( quote::quote! { #[hdk_extern] - pub fn #get_entry_for_entry_function_name(#from_arg_name: #from_hash_type) -> ExternResult> { + pub fn #get_entry_for_entry_function_name(#from_arg_name: #from_field_type) -> ExternResult> { get_links( GetLinksInputBuilder::try_new(#from_arg_name, LinkTypes::#pascal_link_type_name)?.build(), ) @@ -366,28 +399,68 @@ fn get_links_handler_to_entry( } } -fn from_link_hash_type(hash_type: &str) -> TokenStream { - match hash_type { - "AgentPubKey" => quote! { - AgentPubKey::from( - link.target.clone() - .into_entry_hash() - .ok_or(wasm_error!( - WasmErrorInner::Guest("No entry_hash associated with link".to_string()) - ))? - ) - }, - _ => { - let lower_hash_type = hash_type.to_case(Case::Lower); - let into_hash_method_name = format_ident!("into_{}", hash_type.to_case(Case::Snake)); - let error_message = format!("No {lower_hash_type} associated with link"); - quote! { - link.target - .clone() - .#into_hash_method_name() - .ok_or(wasm_error!(WasmErrorInner::Guest(#error_message.to_string())))? +fn get_links_to_any_linkable_hash_handler( + from_referenceable: &Referenceable, + to_referenceable: &Referenceable, + deletable: bool, +) -> TokenStream { + let from_field_type = format_ident!("{}", from_referenceable.field_type().to_string()); + let from_arg_name = format_ident!("{}", from_referenceable.field_name(&Cardinality::Single)); + + let pascal_link_type_name = + format_ident!("{}", link_type_name(from_referenceable, to_referenceable)); + let singular_snake_from_entry_type = format_ident!( + "{}", + from_referenceable + .to_string(&Cardinality::Single) + .to_case(Case::Snake) + ); + let plural_snake_to_entry_type = format_ident!( + "{}", + to_referenceable + .to_string(&Cardinality::Vector) + .to_case(Case::Snake) + ); + + let get_deleted_entry_for_entry_function_name = format_ident!( + "get_deleted_{plural_snake_to_entry_type}_for_{singular_snake_from_entry_type}" + ); + + let get_deleted_links_handler = deletable + .then(|| { + quote::quote! { + #[hdk_extern] + pub fn #get_deleted_entry_for_entry_function_name( + #from_arg_name: #from_field_type, + ) -> ExternResult)>> { + let details = get_link_details( + #from_arg_name, + LinkTypes::#pascal_link_type_name, + None, + GetOptions::default(), + )?; + Ok(details + .into_inner() + .into_iter() + .filter(|(_link, deletes)| !deletes.is_empty()) + .collect()) + } } + }) + .unwrap_or_default(); + + let get_entry_for_entry_function_name = + format_ident!("get_{plural_snake_to_entry_type}_for_{singular_snake_from_entry_type}"); + + quote::quote! { + #[hdk_extern] + pub fn #get_entry_for_entry_function_name(#from_arg_name: #from_field_type) -> ExternResult> { + get_links( + GetLinksInputBuilder::try_new(#from_arg_name, LinkTypes::#pascal_link_type_name)?.build(), + ) } + + #get_deleted_links_handler } } @@ -396,7 +469,7 @@ fn remove_link_handlers( to_referenceable: &Referenceable, bidirectional: bool, ) -> TokenStream { - let from_hash_type = from_referenceable.hash_type().to_string(); + let from_field_type = from_referenceable.field_type().to_string(); let from_arg_name = from_referenceable.field_name(&Cardinality::Single); let inverse_link_type_name = @@ -416,49 +489,54 @@ fn remove_link_handlers( let to_arg_name = format_ident!("{}", to_referenceable.field_name(&Cardinality::Single)); - let remove_entry_for_entry_struct_name = format_ident!( + let remove_link_for_link_struct_name = format_ident!( "Remove{singular_pascal_to_entry_type}For{singular_pascal_from_entry_type}Input" ); let base_field_name = format_ident!("base_{from_arg_name}"); - let from_hash_type = format_ident!("{from_hash_type}"); + let from_field_type = format_ident!("{from_field_type}"); let target_field_name = format_ident!("target_{to_arg_name}"); - let to_hash_type = format_ident!("{}", to_referenceable.hash_type().to_string()); + let to_field_type = format_ident!("{}", to_referenceable.field_type().to_string()); - let remove_entry_for_entry_function_name = - format_ident!("remove_{singular_snake_to_entry_type}_for_{singular_snake_from_entry_type}"); + let delete_link_for_link_function_name = + format_ident!("delete_{singular_snake_to_entry_type}_for_{singular_snake_from_entry_type}"); let pascal_link_type_name = format_ident!("{}", link_type_name(from_referenceable, to_referenceable)); - let from_link = from_link_hash_type(&to_hash_type.to_string()); - let from_inverse = from_link_hash_type(&from_hash_type.to_string()); + let from_link_hash_type_code = hash_type_code_from_referenceable(to_referenceable); - let bidirectional_remove = bidirectional.then(|| - quote! { - let links = get_links( - GetLinksInputBuilder::try_new(input.#target_field_name.clone(), LinkTypes::#inverse_link_type_name)?.build(), - )?; - for link in links { - if #from_inverse == input.#base_field_name { - delete_link(link.create_link_hash)?; + let bidirectional_remove = bidirectional + .then(|| { + let from_inverse_hash_type = hash_type_code_from_referenceable(from_referenceable); + + quote! { + let links = get_links( + GetLinksInputBuilder::try_new( + input.#target_field_name.clone(), + LinkTypes::#inverse_link_type_name)?.build(), + )?; + for link in links { + if #from_inverse_hash_type == input.#base_field_name.clone().into_hash().into() { + delete_link(link.create_link_hash)?; + } } } - } - ).unwrap_or_default(); + }) + .unwrap_or_default(); quote! { #[derive(Serialize, Deserialize, Debug)] - pub struct #remove_entry_for_entry_struct_name { - pub #base_field_name: #from_hash_type, - pub #target_field_name: #to_hash_type, + pub struct #remove_link_for_link_struct_name { + pub #base_field_name: #from_field_type, + pub #target_field_name: #to_field_type, } #[hdk_extern] - pub fn #remove_entry_for_entry_function_name(input: #remove_entry_for_entry_struct_name) -> ExternResult<()> { + pub fn #delete_link_for_link_function_name(input: #remove_link_for_link_struct_name) -> ExternResult<()> { let links = get_links( GetLinksInputBuilder::try_new(input.#base_field_name.clone(), LinkTypes::#pascal_link_type_name)?.build(), )?; for link in links { - if #from_link == input.#target_field_name { + if #from_link_hash_type_code == input.#target_field_name.clone().into_hash().into() { delete_link(link.create_link_hash)?; } } @@ -468,35 +546,34 @@ fn remove_link_handlers( } } -fn normal_handlers( - integrity_zome_name: &str, - from_referenceable: &Referenceable, - to_referenceable: &Referenceable, - delete: bool, - bidirectional: bool, -) -> TokenStream { - let inverse_get = bidirectional - .then(|| get_links_handler(to_referenceable, from_referenceable, delete)) - .unwrap_or_default(); - - let delete_link_handler = delete - .then(|| remove_link_handlers(from_referenceable, to_referenceable, bidirectional)) - .unwrap_or_default(); - - let integrity_zome_name = format_ident!("{integrity_zome_name}"); - let add_links_handler = add_link_handler(from_referenceable, to_referenceable, bidirectional); - let get_links_handler = get_links_handler(from_referenceable, to_referenceable, delete); - - quote! { - use hdk::prelude::*; - use #integrity_zome_name::*; - - #add_links_handler - - #get_links_handler - - #inverse_get +fn hash_type_code_from_referenceable(referenceable: &Referenceable) -> TokenStream { + match referenceable { + Referenceable::Agent { .. } => quote! { + AgentPubKey::from( + link.target.clone() + .into_entry_hash() + .ok_or(wasm_error!( + WasmErrorInner::Guest("No entry_hash associated with link".to_string()) + ))? + ) + }, + Referenceable::ExternalHash { .. } => quote! { + link.target.clone().into_hash() + }, + Referenceable::EntryType(_) => { + let field_type = referenceable.field_type().to_string(); + let into_hash_method_name = format_ident!("into_{}", field_type.to_case(Case::Snake)); + let error_message = format!( + "No {} associated with link", + field_type.to_case(Case::Lower) + ); - #delete_link_handler + quote! { + link.target + .clone() + .#into_hash_method_name() + .ok_or(wasm_error!(WasmErrorInner::Guest(#error_message.to_string())))? + } + } } } diff --git a/src/scaffold/link_type/integrity.rs b/src/scaffold/link_type/integrity.rs index 197a749c1..d9d86cc95 100644 --- a/src/scaffold/link_type/integrity.rs +++ b/src/scaffold/link_type/integrity.rs @@ -202,12 +202,12 @@ pub fn add_link_type_to_integrity_zome( let validate_delete_result: TokenStream = if delete { quote! { - /// TODO: add the appropriate validation rules + // TODO: add the appropriate validation rules Ok(ValidateCallbackResult::Valid) } } else { quote! { - Ok(ValidateCallbackResult::Invalid(String::from(#deleted_invalid_reason))) + Ok(ValidateCallbackResult::Invalid(#deleted_invalid_reason.to_string())) } }; @@ -240,7 +240,7 @@ pub fn add_link_type_to_integrity_zome( #validate_create_to - /// TODO: add the appropriate validation rules + // TODO: add the appropriate validation rules Ok(ValidateCallbackResult::Valid) } }; diff --git a/src/scaffold/web_app.rs b/src/scaffold/web_app.rs index 5d06883c1..a4338da71 100644 --- a/src/scaffold/web_app.rs +++ b/src/scaffold/web_app.rs @@ -3,7 +3,7 @@ use package_manager::PackageManager; use std::path::PathBuf; use crate::error::ScaffoldResult; -use crate::reserved_words::check_for_reserved_words; +use crate::reserved_words::check_for_reserved_keywords; use crate::templates::web_app::scaffold_web_app_template; use crate::templates::ScaffoldedTemplate; use crate::{error::ScaffoldError, file_tree::FileTree}; @@ -26,7 +26,7 @@ pub fn scaffold_web_app( template_file_tree: &FileTree, holo_enabled: bool, ) -> ScaffoldResult { - check_for_reserved_words(app_name)?; + check_for_reserved_keywords(app_name)?; let mut app_file_tree = dir! { ".gitignore" => file!(gitignore()) diff --git a/src/scaffold/zome.rs b/src/scaffold/zome.rs index 6e2bf9b27..57d5cd8a0 100644 --- a/src/scaffold/zome.rs +++ b/src/scaffold/zome.rs @@ -9,7 +9,7 @@ use std::{ use crate::{ file_tree::{build_file_tree, file_exists, insert_file_tree_in_dir, FileTree}, - reserved_words::check_for_reserved_words, + reserved_words::check_for_reserved_keywords, templates::{ coordinator::scaffold_coordinator_zome_templates, integrity::scaffold_integrity_zome_templates, ScaffoldedTemplate, @@ -354,7 +354,7 @@ pub fn scaffold_integrity_zome_with_path( zome_name: &str, path: &Path, ) -> ScaffoldResult { - check_for_reserved_words(zome_name)?; + check_for_reserved_keywords(zome_name)?; let dna_manifest_path = dna_file_tree.dna_manifest_path.clone(); let dna_manifest = dna_file_tree.dna_manifest.clone(); @@ -440,7 +440,7 @@ pub fn scaffold_coordinator_zome_in_path( dependencies: Option<&Vec>, path: &Path, ) -> ScaffoldResult { - check_for_reserved_words(zome_name)?; + check_for_reserved_keywords(zome_name)?; let dna_manifest = dna_file_tree.dna_manifest.clone(); diff --git a/src/templates/link_type.rs b/src/templates/link_type.rs index e5e6138eb..97923e2a8 100644 --- a/src/templates/link_type.rs +++ b/src/templates/link_type.rs @@ -6,7 +6,7 @@ use serde::Serialize; use crate::{ error::ScaffoldResult, file_tree::{file_content, FileTree}, - scaffold::entry_type::definitions::Referenceable, + scaffold::entry_type::definitions::{FieldType, Referenceable}, }; use super::{ @@ -52,6 +52,15 @@ pub fn scaffold_link_type_templates( bidirectional, }; + // This is a measure to prevent UI from getting scaffolded for link-types where the base + // is an ExternalHash since it would expect an Detail component to exist + // which is not possible + let should_skip_ui_gen = no_ui + || to_referenceable + .as_ref() + .map(|r| r.field_type() == FieldType::ExternalHash) + .unwrap_or_default(); + let h = build_handlebars(template_file_tree)?; let link_type_path = PathBuf::from("link-type"); @@ -59,7 +68,7 @@ pub fn scaffold_link_type_templates( if let Some(link_type_template) = template_file_tree.path(&mut v.iter()) { let mut link_type_template = link_type_template.clone(); - if no_ui { + if should_skip_ui_gen { link_type_template.dir_content_mut().map(|v| { v.retain(|k, _| k != "ui"); v diff --git "a/templates/custom-template/custom-template/template/link-type/tests/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if to_referenceable}}{{kebab_case from_referenceable.name}}-to-{{kebab_case (plural to_referenceable.name)}}.test.ts{{\302\241if}}.hbs" "b/templates/custom-template/custom-template/template/link-type/tests/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if to_referenceable}}{{kebab_case from_referenceable.name}}-to-{{kebab_case (plural to_referenceable.name)}}.test.ts{{\302\241if}}.hbs" index 33d8388a6..902aebe29 100644 --- "a/templates/custom-template/custom-template/template/link-type/tests/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if to_referenceable}}{{kebab_case from_referenceable.name}}-to-{{kebab_case (plural to_referenceable.name)}}.test.ts{{\302\241if}}.hbs" +++ "b/templates/custom-template/custom-template/template/link-type/tests/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if to_referenceable}}{{kebab_case from_referenceable.name}}-to-{{kebab_case (plural to_referenceable.name)}}.test.ts{{\302\241if}}.hbs" @@ -17,7 +17,7 @@ test('link a {{pascal_case from_referenceable.name}} to a {{pascal_case to_refer // This assumes app bundle created by the `hc app pack` command. const testAppPath = process.cwd() + '/../workdir/{{app_name}}.happ'; - // Set up the app to be installed + // Set up the app to be installed const appSource = { appBundleSource: { path: testAppPath } }; // Add 2 players with the test app to the Scenario. The returned players @@ -66,9 +66,9 @@ test('link a {{pascal_case from_referenceable.name}} to a {{pascal_case to_refer target_{{snake_case to_referenceable.singular_arg}}: targetAddress } }); - + await dhtSync([alice, bob], alice.cells[0].cell_id[0]); - + // Bob gets the links again linksOutput = await bob.cells[0].callZome({ zome_name: "{{coordinator_zome_manifest.name}}", @@ -97,13 +97,13 @@ test('link a {{pascal_case from_referenceable.name}} to a {{pascal_case to_refer {{#if delete}} await alice.cells[0].callZome({ zome_name: "{{coordinator_zome_manifest.name}}", - fn_name: "remove_{{snake_case to_referenceable.name}}_for_{{snake_case from_referenceable.name}}", + fn_name: "delete_{{snake_case to_referenceable.name}}_for_{{snake_case from_referenceable.name}}", payload: { base_{{snake_case from_referenceable.singular_arg}}: baseAddress, target_{{snake_case to_referenceable.singular_arg}}: targetAddress } }); - + await dhtSync([alice, bob], alice.cells[0].cell_id[0]); // Bob gets the links again @@ -143,4 +143,3 @@ test('link a {{pascal_case from_referenceable.name}} to a {{pascal_case to_refer {{/if}} }); }); - diff --git a/templates/generic/coordinator-zome/tests/src/{{dna_role_name}}/{{zome_manifest.name}}/common.ts.hbs b/templates/generic/coordinator-zome/tests/src/{{dna_role_name}}/{{zome_manifest.name}}/common.ts.hbs index 04f613ed8..437468a51 100644 --- a/templates/generic/coordinator-zome/tests/src/{{dna_role_name}}/{{zome_manifest.name}}/common.ts.hbs +++ b/templates/generic/coordinator-zome/tests/src/{{dna_role_name}}/{{zome_manifest.name}}/common.ts.hbs @@ -1,2 +1,2 @@ import { CallableCell } from '@holochain/tryorama'; -import { NewEntryAction, ActionHash, Record, AppBundleSource, fakeActionHash, fakeAgentPubKey, fakeEntryHash, fakeDnaHash } from '@holochain/client'; +import { NewEntryAction, ActionHash, Record, AppBundleSource, fakeActionHash, fakeAgentPubKey, fakeEntryHash, fakeDnaHash, hashFrom32AndType } from '@holochain/client'; \ No newline at end of file diff --git a/templates/generic/field-types/ExternalHash/sample.hbs b/templates/generic/field-types/ExternalHash/sample.hbs new file mode 100644 index 000000000..f974f7e5f --- /dev/null +++ b/templates/generic/field-types/ExternalHash/sample.hbs @@ -0,0 +1 @@ +hashFrom32AndType(new Uint8Array(39).fill(1), "External") \ No newline at end of file diff --git a/templates/generic/field-types/ExternalHash/type.hbs b/templates/generic/field-types/ExternalHash/type.hbs new file mode 100644 index 000000000..0a299167d --- /dev/null +++ b/templates/generic/field-types/ExternalHash/type.hbs @@ -0,0 +1 @@ +Uint8Array \ No newline at end of file diff --git "a/templates/generic/link-type/tests/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if to_referenceable}}{{kebab_case from_referenceable.name}}-to-{{kebab_case (plural to_referenceable.name)}}.test.ts{{\302\241if}}.hbs" "b/templates/generic/link-type/tests/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if to_referenceable}}{{kebab_case from_referenceable.name}}-to-{{kebab_case (plural to_referenceable.name)}}.test.ts{{\302\241if}}.hbs" index 3812d9fbf..99be6e389 100644 --- "a/templates/generic/link-type/tests/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if to_referenceable}}{{kebab_case from_referenceable.name}}-to-{{kebab_case (plural to_referenceable.name)}}.test.ts{{\302\241if}}.hbs" +++ "b/templates/generic/link-type/tests/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if to_referenceable}}{{kebab_case from_referenceable.name}}-to-{{kebab_case (plural to_referenceable.name)}}.test.ts{{\302\241if}}.hbs" @@ -12,15 +12,20 @@ import { AppBundleSource, fakeActionHash, fakeAgentPubKey, - fakeEntryHash + fakeEntryHash, + hashFrom32AndType } from '@holochain/client'; import { decode } from '@msgpack/msgpack'; -{{#if (ne from_referenceable.hash_type "AgentPubKey")}} +{{#if (ne from_referenceable.hash_type "AgentPubKey") }} + {{#if (ne from_referenceable.hash_type "ExternalHash")}} import { create{{pascal_case from_referenceable.name}} } from './common.js'; + {{/if}} {{/if}} -{{#if (ne to_referenceable.hash_type "AgentPubKey")}} +{{#if (ne to_referenceable.hash_type "AgentPubKey") }} + {{#if (ne to_referenceable.hash_type "ExternalHash")}} import { create{{pascal_case to_referenceable.name}} } from './common.js'; + {{/if}} {{/if}} test('link a {{pascal_case from_referenceable.name}} to a {{pascal_case to_referenceable.name}}', async () => { @@ -29,7 +34,7 @@ test('link a {{pascal_case from_referenceable.name}} to a {{pascal_case to_refer // This assumes app bundle created by the `hc app pack` command. const testAppPath = process.cwd() + '/../workdir/{{app_name}}.happ'; - // Set up the app to be installed + // Set up the app to be installed const appSource = { appBundleSource: { path: testAppPath } }; // Add 2 players with the test app to the Scenario. The returned players @@ -43,21 +48,34 @@ test('link a {{pascal_case from_referenceable.name}} to a {{pascal_case to_refer {{#if (eq from_referenceable.hash_type "AgentPubKey")}} const baseAddress = alice.agentPubKey; {{else}} + {{#if (ne from_referenceable.hash_type "ExternalHash")}} const baseRecord = await create{{pascal_case from_referenceable.name}}(alice.cells[0]); + {{/if}} {{#if (eq from_referenceable.hash_type "EntryHash")}} const baseAddress = (baseRecord.signed_action.hashed.content as NewEntryAction).entry_hash; {{else}} + {{#if (eq from_referenceable.hash_type "ExternalHash")}} + const baseAddress = hashFrom32AndType(new Uint8Array(39).fill(1), "External"); + {{else}} const baseAddress = baseRecord.signed_action.hashed.hash; + {{/if}} {{/if}} {{/if}} {{#if (eq to_referenceable.hash_type "AgentPubKey")}} const targetAddress = alice.agentPubKey; {{else}} + {{#if (ne to_referenceable.hash_type "ExternalHash")}} const targetRecord = await create{{pascal_case to_referenceable.name}}(alice.cells[0]); + {{else}} + {{/if}} {{#if (eq to_referenceable.hash_type "EntryHash")}} const targetAddress = (targetRecord.signed_action.hashed.content as NewEntryAction).entry_hash; {{else}} + {{#if (eq to_referenceable.hash_type "ExternalHash")}} + const targetAddress = hashFrom32AndType(new Uint8Array(39).fill(1), "External"); + {{else}} const targetAddress = targetRecord.signed_action.hashed.hash; + {{/if}} {{/if}} {{/if}} @@ -78,9 +96,9 @@ test('link a {{pascal_case from_referenceable.name}} to a {{pascal_case to_refer target_{{snake_case to_referenceable.singular_arg}}: targetAddress } }); - + await dhtSync([alice, bob], alice.cells[0].cell_id[0]); - + // Bob gets the links again linksOutput = await bob.cells[0].callZome({ zome_name: "{{coordinator_zome_manifest.name}}", @@ -109,13 +127,13 @@ test('link a {{pascal_case from_referenceable.name}} to a {{pascal_case to_refer {{#if delete}} await alice.cells[0].callZome({ zome_name: "{{coordinator_zome_manifest.name}}", - fn_name: "remove_{{snake_case to_referenceable.name}}_for_{{snake_case from_referenceable.name}}", + fn_name: "delete_{{snake_case to_referenceable.name}}_for_{{snake_case from_referenceable.name}}", payload: { base_{{snake_case from_referenceable.singular_arg}}: baseAddress, target_{{snake_case to_referenceable.singular_arg}}: targetAddress } }); - + await dhtSync([alice, bob], alice.cells[0].cell_id[0]); // Bob gets the links again @@ -154,4 +172,4 @@ test('link a {{pascal_case from_referenceable.name}} to a {{pascal_case to_refer {{/if}} }); -}); +}); \ No newline at end of file diff --git a/templates/ui-frameworks/lit/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs b/templates/ui-frameworks/lit/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs index 1781515ec..ba777e2ba 100644 --- a/templates/ui-frameworks/lit/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs +++ b/templates/ui-frameworks/lit/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs @@ -1,9 +1,10 @@ -import { - Record, - ActionHash, +import { + Record, + ActionHash, DnaHash, SignedActionHashed, - EntryHash, + EntryHash, + ExternalHash, AgentPubKey, Create, Update, @@ -35,4 +36,4 @@ export type {{pascal_case zome_manifest.name}}Signal = { link_type: string; }; -export type EntryTypes = {}; +export type EntryTypes = {}; \ No newline at end of file diff --git a/templates/ui-frameworks/react/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs b/templates/ui-frameworks/react/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs index 78ccdd343..e2ebde021 100644 --- a/templates/ui-frameworks/react/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs +++ b/templates/ui-frameworks/react/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs @@ -1,9 +1,11 @@ -import type { - Record, +import type { + Record, ActionHash, DnaHash, SignedActionHashed, - EntryHash, + EntryHash, + ExternalHash, + ExternalHash, AgentPubKey, Create, Update, @@ -35,4 +37,4 @@ export type {{pascal_case zome_manifest.name}}Signal = { link_type: string; }; -export type EntryTypes = {}; +export type EntryTypes = {}; \ No newline at end of file diff --git "a/templates/ui-frameworks/react/link-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if (and bidireccional (and to_referenceable (ne from_referenceable.hash_type 'AgentPubKey')))}}{{pascal_case (plural from_referenceable.name)}}For{{pascal_case to_referenceable.name}}.tsx{{\302\241if}}.hbs" "b/templates/ui-frameworks/react/link-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if (and bidireccional (and to_referenceable (ne from_referenceable.hash_type 'AgentPubKey')))}}{{pascal_case (plural from_referenceable.name)}}For{{pascal_case to_referenceable.name}}.tsx{{\302\241if}}.hbs" index baacf9704..a9f4c3a3b 100644 --- "a/templates/ui-frameworks/react/link-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if (and bidireccional (and to_referenceable (ne from_referenceable.hash_type 'AgentPubKey')))}}{{pascal_case (plural from_referenceable.name)}}For{{pascal_case to_referenceable.name}}.tsx{{\302\241if}}.hbs" +++ "b/templates/ui-frameworks/react/link-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if (and bidireccional (and to_referenceable (ne from_referenceable.hash_type 'AgentPubKey')))}}{{pascal_case (plural from_referenceable.name)}}For{{pascal_case to_referenceable.name}}.tsx{{\302\241if}}.hbs" @@ -36,7 +36,7 @@ const {{pascal_case (plural from_referenceable.name)}}For{{pascal_case to_refere if (signal.App.zome_name !== '{{coordinator_zome_manifest.name}}') return; const payload = signal.App.payload as {{pascal_case coordinator_zome_manifest.name}}Signal; if (payload.type !== 'LinkCreated') return; - if (payload.link_type !== '{{pascal_case bidirectional}}') return; + if (payload.link_type !== '{{pascal_case link_type_name}}') return; await fetch{{pascal_case (plural from_referenceable.name)}}(); }, [fetch{{pascal_case (plural from_referenceable.name)}}]); @@ -70,4 +70,4 @@ interface {{pascal_case (plural from_referenceable.name)}}For{{pascal_case to_re {{camel_case to_referenceable.singular_arg}}: Uint8Array } -export default {{pascal_case (plural from_referenceable.name)}}For{{pascal_case to_referenceable.name}}; +export default {{pascal_case (plural from_referenceable.name)}}For{{pascal_case to_referenceable.name}}; \ No newline at end of file diff --git "a/templates/ui-frameworks/react/link-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if (and to_referenceable (ne to_referenceable.hash_type 'AgentPubKey'))}}{{pascal_case (plural to_referenceable.name)}}For{{pascal_case from_referenceable.name}}.tsx{{\302\241if}}.hbs" "b/templates/ui-frameworks/react/link-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if (and to_referenceable (ne to_referenceable.hash_type 'AgentPubKey'))}}{{pascal_case (plural to_referenceable.name)}}For{{pascal_case from_referenceable.name}}.tsx{{\302\241if}}.hbs" index 2784e7d2c..8bdac84fc 100644 --- "a/templates/ui-frameworks/react/link-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if (and to_referenceable (ne to_referenceable.hash_type 'AgentPubKey'))}}{{pascal_case (plural to_referenceable.name)}}For{{pascal_case from_referenceable.name}}.tsx{{\302\241if}}.hbs" +++ "b/templates/ui-frameworks/react/link-type/ui/src/{{dna_role_name}}/{{coordinator_zome_manifest.name}}/{{#if (and to_referenceable (ne to_referenceable.hash_type 'AgentPubKey'))}}{{pascal_case (plural to_referenceable.name)}}For{{pascal_case from_referenceable.name}}.tsx{{\302\241if}}.hbs" @@ -32,8 +32,9 @@ const {{pascal_case (plural to_referenceable.name)}}For{{pascal_case from_refere }, [client]); const handleSignal: SignalCb = useCallback(async (signal) => { - if (signal.zome_name !== '{{coordinator_zome_manifest.name}}') return; - const payload = signal.payload as {{pascal_case coordinator_zome_manifest.name}}Signal; + if (!(SignalType.App in signal)) return + if (signal.App.zome_name !== '{{coordinator_zome_manifest.name}}') return; + const payload = signal.App.payload as {{pascal_case coordinator_zome_manifest.name}}Signal; if (payload.type !== 'LinkCreated') return; if (payload.link_type !== '{{pascal_case link_type_name}}') return; await fetch{{pascal_case (plural to_referenceable.name)}}(); @@ -69,4 +70,4 @@ interface {{pascal_case (plural to_referenceable.name)}}For{{pascal_case from_re {{camel_case from_referenceable.singular_arg}}: Uint8Array } -export default {{pascal_case (plural to_referenceable.name)}}For{{pascal_case from_referenceable.name}}; +export default {{pascal_case (plural to_referenceable.name)}}For{{pascal_case from_referenceable.name}}; \ No newline at end of file diff --git a/templates/ui-frameworks/svelte/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs b/templates/ui-frameworks/svelte/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs index 3f801ab76..e213f1dc4 100644 --- a/templates/ui-frameworks/svelte/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs +++ b/templates/ui-frameworks/svelte/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs @@ -4,6 +4,7 @@ import type { DnaHash, SignedActionHashed, EntryHash, + ExternalHash, AgentPubKey, Create, Update, diff --git a/templates/ui-frameworks/vue/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs b/templates/ui-frameworks/vue/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs index 7004830d8..194094e40 100644 --- a/templates/ui-frameworks/vue/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs +++ b/templates/ui-frameworks/vue/coordinator-zome/ui/src/{{dna_role_name}}/{{zome_manifest.name}}/types.ts.hbs @@ -1,9 +1,10 @@ -import { - Record, - ActionHash, +import { + Record, + ActionHash, SignedActionHashed, DnaHash, - EntryHash, + EntryHash, + ExternalHash, AgentPubKey, Create, Update, @@ -35,4 +36,4 @@ export type {{pascal_case zome_manifest.name}}Signal = { link_type: string; }; -export type EntryTypes = {}; +export type EntryTypes = {}; \ No newline at end of file