From 920cb1934853fbd8556c31b01999c557335b797d Mon Sep 17 00:00:00 2001 From: Collins Muriuki Date: Mon, 21 Oct 2024 16:24:13 +0300 Subject: [PATCH] chore: Backport and release 0.4000.0-rc.1 (#396) * Bump crate version * feat: Create links from/to `ExternalHash` (#380) * add ExternalHash field types and option to target this type when scaffolding a link-type * ensure external hash is imported in type declarations * feat: create links from/to AnyLinkableHash * Add AnyLinkablehHash field type templates and add to reserved words * fix failed to resolve errors * improve checking for reserved keywords * fix invalid link-type delete method in test template * simplify reserved_words hashsets * update cli * Extend reserved keywords to check for javascript keywords * Update AnyLinkableHash sample value * Extend reserved words check tests * fix AnyLinkableHash link-type tests * Fix AnyLinkableHash link-type tests and remove redundant AND/OR hbs helpers * update inner_choose_referenceable * /AnyLinkableHash/ExternalHash * Update invalid serserved word error message * Refactor entry/link type utils * Add some context to the [None] option when scaffolding a link-type * /AnyLinkableHash/ExternalHash in link-type template * Fix option placement * Prevent UI from getting generated where the base type of a link is an ExternalHash * ExternalHash links can be bidirectional * Only skip ui if to_referenceable is some and the field_type is of ExternalHash * Remove unnecessary into call in delete link function * Fix rustfmt ci failure * Fix missing conversion * Fix react link-type template * feat: Improve `hc-scaffold entry-type` developer experience (#383) * refactor enum type selection; add option to restart field type selection; refactor FieldType parsing * Improve EntryTypeReference parsing * Improve parse_enum parsing logic * Update field_type parsing logic * fix: Optimize nix flake (#384) * optimize nix flake * Supress clippy warnings * Nix flake update --- Cargo.lock | 2 +- Cargo.toml | 2 +- flake.lock | 24 +- src/cli.rs | 18 +- src/error.rs | 4 +- src/reserved_words.rs | 144 ++++-- src/scaffold/collection.rs | 4 +- src/scaffold/dna.rs | 4 +- src/scaffold/entry_type.rs | 10 +- src/scaffold/entry_type/coordinator.rs | 4 +- src/scaffold/entry_type/definitions.rs | 212 +++++++-- src/scaffold/entry_type/fields.rs | 419 ++++++++---------- src/scaffold/entry_type/utils.rs | 213 ++++----- src/scaffold/link_type.rs | 19 +- src/scaffold/link_type/coordinator.rs | 341 ++++++++------ src/scaffold/link_type/integrity.rs | 6 +- src/scaffold/web_app.rs | 4 +- src/scaffold/zome.rs | 6 +- src/templates/collection.rs | 2 +- src/templates/entry_type.rs | 2 +- src/templates/link_type.rs | 15 +- src/utils.rs | 18 +- ...nceable.name)}}.test.ts{{\302\241if}}.hbs" | 11 +- .../{{zome_manifest.name}}/common.ts.hbs | 2 +- .../field-types/ExternalHash/sample.hbs | 1 + .../generic/field-types/ExternalHash/type.hbs | 1 + ...nceable.name)}}.test.ts{{\302\241if}}.hbs" | 36 +- .../{{zome_manifest.name}}/types.ts.hbs | 11 +- .../{{zome_manifest.name}}/types.ts.hbs | 10 +- ...eferenceable.name}}.tsx{{\302\241if}}.hbs" | 4 +- ...eferenceable.name}}.tsx{{\302\241if}}.hbs" | 7 +- .../{{zome_manifest.name}}/types.ts.hbs | 1 + .../{{zome_manifest.name}}/types.ts.hbs | 11 +- 33 files changed, 908 insertions(+), 660 deletions(-) create mode 100644 templates/generic/field-types/ExternalHash/sample.hbs create mode 100644 templates/generic/field-types/ExternalHash/type.hbs diff --git a/Cargo.lock b/Cargo.lock index 9bacd5419..cc1f9226b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3028,7 +3028,7 @@ dependencies = [ [[package]] name = "holochain_scaffolding_cli" -version = "0.4000.0-rc.0" +version = "0.4000.0-rc.1" dependencies = [ "anyhow", "build-fs-tree", diff --git a/Cargo.toml b/Cargo.toml index f23fd86bb..f245f39d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "holochain_scaffolding_cli" -version = "0.4000.0-rc.0" +version = "0.4000.0-rc.1" description = "CLI to easily generate and modify holochain apps" license = "CAL-1.0" homepage = "https://developer.holochain.org" diff --git a/flake.lock b/flake.lock index 3c7a56ddb..830d74a44 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1728344376, - "narHash": "sha256-lxTce2XE6mfJH8Zk6yBbqsbu9/jpwdymbSH5cCbiVOA=", + "lastModified": 1728776144, + "narHash": "sha256-fROVjMcKRoGHofDm8dY3uDUtCMwUICh/KjBFQnuBzfg=", "owner": "ipetkov", "repo": "crane", - "rev": "fd86b78f5f35f712c72147427b1eb81a9bd55d0b", + "rev": "f876e3d905b922502f031aeec1a84490122254b7", "type": "github" }, "original": { @@ -53,11 +53,11 @@ "hc-scaffold": { "flake": false, "locked": { - "lastModified": 1727375207, - "narHash": "sha256-wGS+cOhvakLWscqPI0LaBZVZ3ryORV3YDvL+bfhI+WA=", + "lastModified": 1728659843, + "narHash": "sha256-3A15jw8uf5t8mONlAtJngDB+n0wNKA3cB+lZ2mHF/RY=", "owner": "holochain", "repo": "scaffolding", - "rev": "b218f253a124b6e7b5be0600c3aab7a57344f0f2", + "rev": "d36abccbfbff833a8eb73b38be848856b0d38f53", "type": "github" }, "original": { @@ -104,11 +104,11 @@ ] }, "locked": { - "lastModified": 1728325228, - "narHash": "sha256-On5Kto/s/mEqHm1QfJ1+5FSwkQw4E7ArHVj2ImO58t0=", + "lastModified": 1729097922, + "narHash": "sha256-HqzaJJ8BFpUitdW76puLZ91Byp8lGoYIUl5fGdsUarM=", "owner": "holochain", "repo": "holonix", - "rev": "6df6c63e5c06720289a7e7376deec94b620af470", + "rev": "81f74bf1445b17fe0c5d556936c6df837ea54ba9", "type": "github" }, "original": { @@ -179,11 +179,11 @@ ] }, "locked": { - "lastModified": 1728613723, - "narHash": "sha256-zVVj0PKguM8ZMdLE43YW7dzer3tl9e6i5Qs1fr878+c=", + "lastModified": 1729132166, + "narHash": "sha256-Mhl4T7gDGknG4nPbHNSGWynfSjZeoWBdsaIzhUYuIlU=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "ca93f28abd2147dd9997261dcaeacc5a30dba463", + "rev": "32d889f9b9fc65cb65aa2d5db282d60ed06f348e", "type": "github" }, "original": { 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..6e946b413 100644 --- a/src/scaffold/entry_type/definitions.rs +++ b/src/scaffold/entry_type/definitions.rs @@ -1,13 +1,20 @@ -use std::str::FromStr; - +use anyhow::Context; +use colored::Colorize; use convert_case::{Case, Casing}; +use holochain::test_utils::itertools::Itertools; use proc_macro2::TokenStream; use quote::{format_ident, quote}; +use regex::Regex; use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; +use std::str::FromStr; -use crate::{error::ScaffoldError, reserved_words::check_for_reserved_words, utils::check_case}; +use crate::{ + error::{ScaffoldError, ScaffoldResult}, + 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,22 +31,19 @@ pub enum FieldType { ActionHash, EntryHash, DnaHash, + ExternalHash, Enum { label: String, variants: Vec, }, } -impl TryFrom for FieldType { - type Error = ScaffoldError; - - fn try_from(value: String) -> Result { - let list = FieldType::list(); +impl FromStr for FieldType { + type Err = ScaffoldError; - for el in list { - if value.eq(&el.to_string()) { - return Ok(el); - } + fn from_str(s: &str) -> Result { + if let Some(f) = FieldType::list().iter().find(|v| s == v.to_string()) { + return Ok(f.to_owned()); } Err(ScaffoldError::InvalidArguments(format!( @@ -64,6 +68,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 +88,7 @@ impl FieldType { FieldType::ActionHash, FieldType::EntryHash, FieldType::DnaHash, + FieldType::ExternalHash, FieldType::AgentPubKey, FieldType::Enum { label: String::new(), @@ -91,6 +97,24 @@ impl FieldType { ] } + pub fn parse_enum(fields_str: &str) -> ScaffoldResult { + let mut str_path = fields_str.split(':'); + + let variants = str_path + .next_back() + .context(format!("Enum variants missing from: {}", fields_str))?; + let variants = variants + .split('.') + .map(|v| v.to_case(Case::Pascal)) + .collect::>(); + let label = str_path + .next_back() + .context(format!("Enum label missing from: {}", fields_str))? + .to_string(); + + Ok(FieldType::Enum { label, variants }) + } + pub fn rust_type(&self) -> TokenStream { use FieldType::*; @@ -104,6 +128,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 +151,7 @@ impl FieldType { ActionHash => "ActionHash", EntryHash => "EntryHash", DnaHash => "DnaHash", + ExternalHash => "ExternalHash", Enum { label, .. } => label, } } @@ -186,7 +212,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, @@ -215,6 +241,89 @@ impl FieldDefinition { } } +impl FromStr for FieldDefinition { + type Err = ScaffoldError; + + fn from_str(fields_str: &str) -> Result { + let mut str_path = fields_str.split(':'); + + let field_name = str_path.next().context(format!( + "field_name is missing from: {}\nExample: \"{}\"", + fields_str, + "title:String".italic() + ))?; + check_case(field_name, "field_name", Case::Snake)?; + + let field_type_str = str_path.next().context(format!( + "{} is missing a field_type, use one of: {}\nExample: \"{}\"", + field_name, + FieldType::list() + .iter() + .map(|f| f.to_string()) + .join(", ") + .italic(), + "title:String".italic() + ))?; + + let vec_regex = Regex::new(r"Vec<(?P(.)*)>\z").unwrap(); + let option_regex = Regex::new(r"Option<(?P(.)*)>\z").unwrap(); + + let (field_type, cardinality) = if vec_regex.is_match(field_type_str) { + let field_type = vec_regex.replace(field_type_str, "${a}"); + + if field_type == "Enum" { + (FieldType::parse_enum(fields_str)?, Cardinality::Vector) + } else { + (FieldType::from_str(&field_type)?, Cardinality::Vector) + } + } else if option_regex.is_match(field_type_str) { + let field_type = option_regex.replace(field_type_str, "${a}"); + + if field_type == "Enum" { + (FieldType::parse_enum(fields_str)?, Cardinality::Option) + } else { + (FieldType::from_str(&field_type)?, Cardinality::Option) + } + } else if field_type_str == "Enum" { + (FieldType::parse_enum(fields_str)?, Cardinality::Single) + } else { + (FieldType::from_str(field_type_str)?, Cardinality::Single) + }; + + // XXX: perhaps widget-types can be validated at this level rather than + // on attemting to render templates + let widget = str_path + .next() + .filter(|v| !v.is_empty()) + .map(|v| v.to_string()); + + let linked_from = str_path + .next() + .filter(|v| !v.is_empty()) + .map(|v| match field_type { + FieldType::AgentPubKey => Some(Referenceable::Agent { + role: v.to_string(), + }), + FieldType::EntryHash | FieldType::ActionHash => { + Some(Referenceable::EntryType(EntryTypeReference { + entry_type: v.to_string(), + reference_entry_hash: matches!(field_type, FieldType::EntryHash), + })) + } + _ => None, + }) + .unwrap_or_default(); + + FieldDefinition::new( + field_name.to_string(), + field_type, + widget, + cardinality, + linked_from, + ) + } +} + #[derive(Serialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct EntryTypeReference { pub entry_type: String, @@ -222,7 +331,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 +349,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), @@ -252,31 +361,29 @@ impl FromStr for EntryTypeReference { type Err = ScaffoldError; fn from_str(s: &str) -> Result { - let sp: Vec<&str> = s.split(':').collect(); - check_case(sp[0], "entry type reference", Case::Snake)?; - - let reference_entry_hash = match sp.len() { - 0 | 1 => false, - _ => match sp[1] { - "EntryHash" => true, - "ActionHash" => false, - _ => Err(ScaffoldError::InvalidArguments(String::from( - "second argument for reference type must be \"EntryHash\" or \"ActionHash\"", - )))?, - }, - }; + let mut str_path = s.split(':'); + + let entry_type = str_path + .next() + .context(format!("Failed to parse entry_type from: {}", s))?; + + let reference_entry_hash = str_path + .next() + .map(|v| matches!(v, "EntryHash")) + .unwrap_or_default(); Ok(EntryTypeReference { - entry_type: sp[0].to_string().to_case(Case::Pascal), + entry_type: entry_type.to_case(Case::Pascal), reference_entry_hash, }) } } -#[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 +393,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 +403,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 +441,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 +450,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/fields.rs b/src/scaffold/entry_type/fields.rs index ef57e31b8..1e7669d38 100644 --- a/src/scaffold/entry_type/fields.rs +++ b/src/scaffold/entry_type/fields.rs @@ -1,16 +1,15 @@ -use std::{path::PathBuf, str::FromStr}; +use std::path::PathBuf; +use colored::Colorize; use convert_case::{Case, Casing}; use dialoguer::{theme::ColorfulTheme, Confirm, Select}; -use regex::Regex; use crate::{ error::{ScaffoldError, ScaffoldResult}, file_tree::{dir_content, FileTree}, + reserved_words::check_for_reserved_keywords, scaffold::zome::ZomeFileTree, - utils::{ - check_case, input_with_case, input_with_case_and_initial_text, input_with_custom_validation, - }, + utils::{check_case, input_with_case, input_with_custom_validation}, }; use super::{ @@ -18,140 +17,65 @@ use super::{ integrity::get_all_entry_types, }; -impl FromStr for FieldDefinition { - type Err = ScaffoldError; - - fn from_str(fields_str: &str) -> Result { - let sp: Vec<&str> = fields_str.split(':').collect(); - - let field_name = sp[0].to_string(); - - check_case(&field_name, "field_name", Case::Snake)?; - - let field_type_str = sp[1].to_string(); - - let vec_regex = Regex::new(r"Vec<(?P(.)*)>\z").unwrap(); - let option_regex = Regex::new(r"Option<(?P(.)*)>\z").unwrap(); - - let (field_type, cardinality) = if vec_regex.is_match(field_type_str.as_str()) { - let field_type = vec_regex.replace(field_type_str.as_str(), "${a}"); - - if field_type == "Enum" { - (parse_enum(fields_str)?, Cardinality::Vector) - } else { - ( - FieldType::try_from(field_type.to_string())?, - Cardinality::Vector, - ) - } - } else if option_regex.is_match(field_type_str.as_str()) { - let field_type = option_regex.replace(field_type_str.as_str(), "${a}"); - - if field_type == "Enum" { - (parse_enum(fields_str)?, Cardinality::Option) - } else { - ( - FieldType::try_from(field_type.to_string())?, - Cardinality::Option, - ) - } - } else if field_type_str == "Enum" { - (parse_enum(fields_str)?, Cardinality::Single) - } else { - ( - FieldType::try_from(field_type_str.to_string())?, - Cardinality::Single, - ) - }; - - let widget = if sp.len() > 2 { - match sp[2] { - "" => None, - _ => Some(sp[2].to_string()), - } - } else { - None - }; - - let linked_from = match field_type { - FieldType::AgentPubKey => match sp.len() { - 4 => Some(Referenceable::Agent { - role: sp[3].to_string(), - }), - _ => None, - }, - FieldType::EntryHash | FieldType::ActionHash => match sp.len() { - 4 => Some(Referenceable::EntryType(EntryTypeReference { - entry_type: sp[3].to_string(), - reference_entry_hash: matches!(field_type, FieldType::EntryHash), - })), - _ => None, - }, - _ => None, - }; - - Ok(FieldDefinition { - field_name, - field_type, - widget, - cardinality, - linked_from, - }) - } -} - -fn parse_enum(fields_str: &str) -> ScaffoldResult { - let sp: Vec<&str> = fields_str.split(':').collect(); - - let label = sp[3].to_string().to_case(Case::Pascal); - - let variants = sp[4].split('.').map(|v| v.to_case(Case::Pascal)).collect(); - - Ok(FieldType::Enum { label, variants }) -} - -pub fn choose_widget( - field_type: &FieldType, +pub fn choose_fields( + entry_type_name: &str, + zome_file_tree: &ZomeFileTree, field_types_templates: &FileTree, -) -> ScaffoldResult> { - let path = PathBuf::new().join(field_type.to_string()); - - match dir_content(field_types_templates, &path) { - Err(_) => Ok(None), - Ok(folders) => { - let widgets_that_can_render_this_type: Vec = folders - .into_iter() - .filter(|(_key, value)| value.dir_content().is_some()) - .map(|(key, _value)| key) - .map(|s| s.to_str().unwrap().to_string()) - .collect(); + no_ui: bool, +) -> ScaffoldResult> { + let mut finished = false; + let mut fields: Vec = Vec::new(); - if widgets_that_can_render_this_type.is_empty() { - return Ok(None); - } + println!("\nWhich fields should the entry contain?\n"); - let visible = Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt("Should this field be visible in the UI?") - .interact()?; + while !finished { + let field_def = choose_field( + entry_type_name, + zome_file_tree, + field_types_templates, + no_ui, + )?; + println!(); - if !visible { - return Ok(None); - } + fields.push(field_def); + finished = !Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Add another field to the entry?") + .report(false) + .interact()?; + } - let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Choose widget to render this field:") - .default(0) - .items(&widgets_that_can_render_this_type[..]) - .interact()?; + println!( + "Chosen fields:\n {}", + fields + .iter() + .map(|f| format!("{}: {}", f.field_name.clone(), f.field_type)) + .collect::>() + .join(", ") + .italic() + ); - let widget_name = widgets_that_can_render_this_type[selection].clone(); + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt( + "Do you want to proceed with the current entry type or restart from the beginning?", + ) + .item("Confirm") + .item("Restart") + .default(0) + .interact()?; - Ok(Some(widget_name)) - } + if selection == 1 { + return choose_fields( + entry_type_name, + zome_file_tree, + field_types_templates, + no_ui, + ); } + + Ok(fields) } -pub fn choose_field( +fn choose_field( entry_type_name: &str, zome_file_tree: &ZomeFileTree, field_types_templates: &FileTree, @@ -164,6 +88,16 @@ pub fn choose_field( .map(|s| s.to_string()) .collect(); + let field_name = input_with_custom_validation("Field name:", |input| { + if let Err(e) = check_case(&input, "field_name", Case::Snake) { + return Err(e.to_string()); + } + if let Err(e) = check_for_reserved_keywords(&input) { + return Err(e.to_string()); + } + Ok(()) + })?; + let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Choose field type:") .default(0) @@ -172,8 +106,8 @@ pub fn choose_field( .item("Vector of...") .interact()?; - // If user selected vector - let (cardinality, mut field_type) = if selection == field_type_names.len() { + // If user selected Vector of ... + let (cardinality, field_type) = if selection == field_type_names.len() { let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Option of which field type?") .default(0) @@ -181,6 +115,7 @@ pub fn choose_field( .interact()?; (Cardinality::Option, field_types[selection].clone()) + // If user selected Option of ... } else if selection == field_type_names.len() + 1 { let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Vector of which field type?") @@ -195,7 +130,7 @@ pub fn choose_field( if let FieldType::Enum { .. } = field_type { let label = - input_with_custom_validation("Enter the name of the enum:", |input: &String| { + input_with_custom_validation("Enter the name of the enum:", |input: String| { if !input.is_case(Case::Pascal) { return Err(format!("Input must be {:?} case.", Case::Pascal)); } @@ -207,53 +142,63 @@ pub fn choose_field( Ok(()) })?; - let mut variants: Vec = Vec::new(); + let mut variants = Vec::new(); + let mut another_variant = true; - let mut another_field = true; - - while another_field { + while another_variant { let variant = input_with_custom_validation( "Enter the name of the next variant:", - |input: &String| { + |input: String| { if !input.is_case(Case::Pascal) { return Err(format!("Input must be {:?} case.", Case::Pascal)); } - if variants.contains(input) { + if variants.contains(&input) { return Err(format!("{input} is already a variant of the enum")); } Ok(()) }, )?; variants.push(variant); - another_field = Confirm::with_theme(&ColorfulTheme::default()) + another_variant = Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Add another variant to the enum?") .report(false) .interact()?; } - field_type = FieldType::Enum { label, variants }; + let widget = (!no_ui) + .then(|| choose_widget(&field_type, field_types_templates)) + .transpose()? + .flatten(); + + return FieldDefinition::new( + label.to_case(Case::Snake), + FieldType::Enum { label, variants }, + widget, + cardinality, + None, + ); } - let maybe_linked_from = match &field_type { + let linked_from = match &field_type { FieldType::AgentPubKey => { - let link_from = Confirm::with_theme(&ColorfulTheme::default()) + let should_link_from_agent_pubkey = Confirm::with_theme(&ColorfulTheme::default()) .with_prompt( "Should a link from the AgentPubKey provided in this field also be created when entries of this type are created?" ) .interact()?; - match link_from { - false => None, - true => { - let role = input_with_case(&String::from( - "Which role does this agent play in the relationship ? (eg. \"creator\", \"invitee\")", - ), Case::Snake)?; - Some(Referenceable::Agent { role }) - } + if should_link_from_agent_pubkey { + let role = input_with_case( + "Which role does this agent play in the relationship ? (eg. \"creator\", \"invitee\")", + Case::Snake + )?; + Some(Referenceable::Agent { role }) + } else { + None } } FieldType::ActionHash | FieldType::EntryHash => { - let link_from = Confirm::with_theme(&ColorfulTheme::default()) + let should_link_from_hash_type = Confirm::with_theme(&ColorfulTheme::default()) .with_prompt( format!( "Should a link from the {field_type} provided in this field also be created when entries of this type are created?", @@ -261,112 +206,100 @@ pub fn choose_field( ) .interact()?; - match link_from { - false => None, - true => { - let all_entry_types = get_all_entry_types(zome_file_tree)?.unwrap_or(vec![]); - let mut all_options: Vec = all_entry_types - .clone() - .into_iter() - .map(|r| r.entry_type) - .collect(); - - if let Cardinality::Option | Cardinality::Vector = cardinality { - all_options.push(format!( - "{} (itself)", - entry_type_name.to_case(Case::Pascal) - )); - } + if should_link_from_hash_type { + let all_entry_types = get_all_entry_types(zome_file_tree)?.unwrap_or_default(); + let mut all_options: Vec = all_entry_types + .clone() + .into_iter() + .map(|r| r.entry_type) + .collect(); + + if let Cardinality::Option | Cardinality::Vector = cardinality { + all_options.push(format!( + "{} (itself)", + entry_type_name.to_case(Case::Pascal) + )); + } - if all_options.is_empty() { - return Err(ScaffoldError::NoEntryTypesDefFoundForIntegrityZome( - zome_file_tree.dna_file_tree.dna_manifest.name(), - zome_file_tree.zome_manifest.name.to_string(), - )); - } + if all_options.is_empty() { + return Err(ScaffoldError::NoEntryTypesDefFoundForIntegrityZome( + zome_file_tree.dna_file_tree.dna_manifest.name(), + zome_file_tree.zome_manifest.name.to_string(), + )); + } - let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt(String::from("Which entry type is this field referring to?")) - .default(0) - .items(&all_options[..]) - .interact()?; - - let reference_entry_hash = matches!(field_type, FieldType::EntryHash); - - match selection == all_entry_types.len() { - true => Some(Referenceable::EntryType(EntryTypeReference { - entry_type: entry_type_name.to_owned(), - reference_entry_hash, - })), - false => Some(Referenceable::EntryType(EntryTypeReference { - entry_type: all_entry_types[selection].entry_type.clone(), - reference_entry_hash, - })), - } + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt(String::from("Which entry type is this field referring to?")) + .default(0) + .items(&all_options[..]) + .interact()?; + + let reference_entry_hash = matches!(field_type, FieldType::EntryHash); + + // if the entry links to itself + if selection == all_entry_types.len() { + Some(Referenceable::EntryType(EntryTypeReference { + entry_type: entry_type_name.to_owned(), + reference_entry_hash, + })) + } else { + Some(Referenceable::EntryType(EntryTypeReference { + entry_type: all_entry_types[selection].entry_type.clone(), + reference_entry_hash, + })) } + } else { + None } } _ => None, }; - let initial_text = match &maybe_linked_from { - Some(r) => r.field_name(&cardinality), - None => String::from(""), - }; - - let field_name: String = - input_with_case_and_initial_text(&String::from("Field name:"), Case::Snake, &initial_text)?; - - let widget = if no_ui { - None - } else { - choose_widget(&field_type, field_types_templates)? - }; + let widget = (!no_ui) + .then(|| choose_widget(&field_type, field_types_templates)) + .transpose()? + .flatten(); - FieldDefinition::new( - field_name, - field_type, - widget, - cardinality, - maybe_linked_from, - ) + FieldDefinition::new(field_name, field_type, widget, cardinality, linked_from) } -pub fn choose_fields( - entry_type_name: &str, - zome_file_tree: &ZomeFileTree, +fn choose_widget( + field_type: &FieldType, field_types_templates: &FileTree, - no_ui: bool, -) -> ScaffoldResult> { - let mut finished = false; - let mut fields: Vec = Vec::new(); - println!("\nWhich fields should the entry contain?\n"); +) -> ScaffoldResult> { + let path = PathBuf::new().join(field_type.to_string()); - while !finished { - let field_def = choose_field( - entry_type_name, - zome_file_tree, - field_types_templates, - no_ui, - )?; - println!(); + match dir_content(field_types_templates, &path) { + Ok(folders) => { + let widgets_that_can_render_this_type: Vec = folders + .into_iter() + .filter(|(_key, value)| value.dir_content().is_some()) + .map(|(key, _value)| key) + .map(|s| s.to_str().unwrap().to_string()) + .collect(); - fields.push(field_def); - finished = !Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt("Add another field to the entry?") - .report(false) - .interact()?; - } + if widgets_that_can_render_this_type.is_empty() { + return Ok(None); + } - println!( - "Chosen fields: {} -", - fields - .iter() - .map(|f| f.field_name.clone()) - .collect::>() - .join(", ") - ); + let should_scaffold_ui = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Should UI be generated for this field?") + .interact()?; - Ok(fields) + if !should_scaffold_ui { + return Ok(None); + } + + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Choose widget to render this field:") + .default(0) + .items(&widgets_that_can_render_this_type[..]) + .interact()?; + + let widget_name = widgets_that_can_render_this_type[selection].clone(); + + Ok(Some(widget_name)) + } + Err(_) => Ok(None), + } } 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/collection.rs b/src/templates/collection.rs index b459e1dcc..38501fae8 100644 --- a/src/templates/collection.rs +++ b/src/templates/collection.rs @@ -28,7 +28,7 @@ pub struct ScaffoldCollectionData { } // TODO: group some params into a new-type or prefer builder pattern -#[allow(clippy::too_many_arguments)] +#[allow(unknown_lints, clippy::too_many_arguments, clippy::manual_inspect)] pub fn scaffold_collection_templates( mut app_file_tree: FileTree, template_file_tree: &FileTree, diff --git a/src/templates/entry_type.rs b/src/templates/entry_type.rs index 680855a25..0f0b78878 100644 --- a/src/templates/entry_type.rs +++ b/src/templates/entry_type.rs @@ -25,7 +25,7 @@ pub struct ScaffoldEntryTypeData<'a> { } // TODO: group some params into a new-type or prefer builder pattern -#[allow(clippy::too_many_arguments)] +#[allow(unknown_lints, clippy::too_many_arguments, clippy::manual_inspect)] pub fn scaffold_entry_type_templates( mut app_file_tree: FileTree, template_file_tree: &FileTree, diff --git a/src/templates/link_type.rs b/src/templates/link_type.rs index 1e9062bea..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::{ @@ -26,7 +26,7 @@ pub struct ScaffoldLinkTypeData<'a> { } // TODO: group some params into a new-type or prefer builder pattern -#[allow(clippy::too_many_arguments)] +#[allow(unknown_lints, clippy::too_many_arguments, clippy::manual_inspect)] pub fn scaffold_link_type_templates( mut app_file_tree: FileTree, template_file_tree: &FileTree, @@ -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/src/utils.rs b/src/utils.rs index cb368a0b1..78909e906 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,8 +5,9 @@ use std::process::Command; use std::{ffi::OsString, path::PathBuf}; use anyhow::Context; +use colored::Colorize; use convert_case::{Case, Casing}; -use dialoguer::{theme::ColorfulTheme, Input, Select, Validator}; +use dialoguer::{theme::ColorfulTheme, Input, Select}; use dprint_plugin_typescript::configuration::ConfigurationBuilder; use crate::error::{ScaffoldError, ScaffoldResult}; @@ -101,16 +102,21 @@ pub fn input_yes_or_no(prompt: &str, recommended: Option) -> ScaffoldResul } #[inline] -pub fn input_with_custom_validation<'a, V>(prompt: &str, validator: V) -> ScaffoldResult +pub fn input_with_custom_validation(prompt: &str, validator: V) -> ScaffoldResult where - V: Validator + 'a, - V::Err: ToString, + V: Fn(String) -> Result<(), String>, { - let input: String = Input::with_theme(&ColorfulTheme::default()) + let mut input: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt(prompt) - .validate_with(validator) .interact_text()?; + while let Err(e) = validator(input.clone()) { + println!("{}", e.red()); + input = Input::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .interact_text()?; + } + Ok(input) } 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