diff --git a/Cargo.lock b/Cargo.lock index 7a96cb5c..34d1d888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4723,6 +4723,7 @@ version = "0.3.0" dependencies = [ "anyhow", "askama", + "clap", "duct", "flate2", "glob", diff --git a/crates/pop-cli/src/commands/new/pallet.rs b/crates/pop-cli/src/commands/new/pallet.rs index d23ca084..d886b40b 100644 --- a/crates/pop-cli/src/commands/new/pallet.rs +++ b/crates/pop-cli/src/commands/new/pallet.rs @@ -1,42 +1,156 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::style::Theme; -use clap::Args; -use cliclack::{clear_screen, confirm, intro, outro, outro_cancel, set_theme}; -use console::style; -use pop_common::manifest::{add_crate_to_workspace, find_workspace_toml}; -use pop_parachains::{create_pallet_template, resolve_pallet_path, TemplatePalletConfig}; -use std::fs; +use crate::{ + cli::{traits::Cli as _, Cli}, + multiselect_pick, +}; + +use clap::{Args, Subcommand}; +use cliclack::{confirm, input, multiselect, outro, outro_cancel}; +use pop_common::{add_crate_to_workspace, find_workspace_toml, prefix_with_current_dir_if_needed}; +use pop_parachains::{ + create_pallet_template, TemplatePalletConfig, TemplatePalletConfigCommonTypes, + TemplatePalletOptions, TemplatePalletStorageTypes, +}; +use std::{fs, path::PathBuf, process::Command}; +use strum::{EnumMessage, IntoEnumIterator}; + +fn after_help_simple() -> &'static str { + r#"Examples: + pop new pallet + -> Will create a simple pallet, you'll have to choose your pallet name. + pop new pallet my-pallet + -> Will automatically create a pallet called my-pallet in the current directory. + pop new pallet pallets/my-pallet + -> Will automatically create a pallet called my pallet in the directory ./pallets + pop new pallet advanced + -> Will unlock the advanced mode. pop new pallet advanced --help for further info. + "# +} + +fn after_help_advanced() -> &'static str { + r#" + Examples: + pop new pallet my-pallet advanced + -> If no [OPTIONS] are specified, the interactive advanced mode is launched. + pop new pallet my-pallet advanced --config-common-types runtime-origin currency --storage storage-value storage-map -d + -> Using some [OPTIONS] will execute the non-interactive advanced mode. + "# +} #[derive(Args)] +#[command(after_help= after_help_simple())] pub struct NewPalletCommand { - #[arg(help = "Name of the pallet", default_value = "pallet-template")] - pub(crate) name: String, + #[arg(help = "Name of the pallet")] + pub(crate) name: Option, #[arg(short, long, help = "Name of authors", default_value = "Anonymous")] pub(crate) authors: Option, #[arg(short, long, help = "Pallet description", default_value = "Frame Pallet")] pub(crate) description: Option, - #[arg(short = 'p', long, help = "Path to the pallet, [default: current directory]")] - pub(crate) path: Option, + #[command(subcommand)] + pub(crate) mode: Option, +} + +#[derive(Subcommand)] +pub enum Mode { + /// Advanced mode enables more detailed customization of pallet development. + Advanced(AdvancedMode), +} + +#[derive(Args)] +#[command(after_help = after_help_advanced())] +pub struct AdvancedMode { + #[arg(short, long, value_enum, num_args(0..), help = "Add common types to your config trait from the CLI.")] + pub(crate) config_common_types: Vec, + #[arg(short, long, help = "Use a default configuration for your config trait.")] + pub(crate) default_config: bool, + #[arg(short, long, value_enum, num_args(0..), help = "Add storage items to your pallet from the CLI.")] + pub(crate) storage: Vec, + #[arg(short, long, help = "Add a genesis config to your pallet.")] + pub(crate) genesis_config: bool, + #[arg(short = 'o', long, help = "Add a custom origin to your pallet.")] + pub(crate) custom_origin: bool, } impl NewPalletCommand { /// Executes the command. pub(crate) async fn execute(self) -> anyhow::Result<()> { - clear_screen()?; - intro(format!( - "{}: Generating new pallet \"{}\"!", - style(" Pop CLI ").black().on_magenta(), - &self.name, - ))?; - set_theme(Theme); - let target = resolve_pallet_path(self.path.clone())?; + Cli.intro("Generate a pallet")?; + + let mut pallet_default_config = false; + let mut pallet_common_types = Vec::new(); + let mut pallet_storage = Vec::new(); + let mut pallet_genesis = false; + let mut pallet_custom_origin = false; + + if let Some(Mode::Advanced(advanced_mode_args)) = &self.mode { + if advanced_mode_args.config_common_types.is_empty() + && advanced_mode_args.storage.is_empty() + && !(advanced_mode_args.genesis_config + || advanced_mode_args.default_config + || advanced_mode_args.custom_origin) + { + Cli.info("Generate the pallet's config trait.")?; + + pallet_common_types = multiselect_pick!(TemplatePalletConfigCommonTypes, "Are you interested in adding one of these types and their usual configuration to your pallet?"); + Cli.info("Generate the pallet's storage.")?; + + pallet_storage = multiselect_pick!( + TemplatePalletStorageTypes, + "Are you interested in adding some of those storage items to your pallet?" + ); + + // If there's no common types, default_config is excluded from the multiselect + let boolean_options = if pallet_common_types.is_empty() { + multiselect_pick!( + TemplatePalletOptions, + "Are you interested in adding one of these types and their usual configuration to your pallet?", + vec![TemplatePalletOptions::DefaultConfig] + ) + } else { + multiselect_pick!( + TemplatePalletOptions, + "Are you interested in adding one of these types and their usual configuration to your pallet?" + ) + }; + + pallet_default_config = + boolean_options.contains(&TemplatePalletOptions::DefaultConfig); + pallet_genesis = boolean_options.contains(&TemplatePalletOptions::GenesisConfig); + pallet_custom_origin = + boolean_options.contains(&TemplatePalletOptions::CustomOrigin); + } else { + pallet_common_types = advanced_mode_args.config_common_types.clone(); + pallet_default_config = advanced_mode_args.default_config; + if pallet_common_types.is_empty() && pallet_default_config { + return Err(anyhow::anyhow!( + "Specify at least a config common type to use default config." + )); + } + pallet_storage = advanced_mode_args.storage.clone(); + pallet_genesis = advanced_mode_args.genesis_config; + pallet_custom_origin = advanced_mode_args.custom_origin; + } + }; + + let pallet_path = if let Some(path) = self.name { + PathBuf::from(path) + } else { + let path: String = input("Where should your project be created?") + .placeholder("./template") + .default_input("./template") + .interact()?; + PathBuf::from(path) + }; + + // If the user has introduced something like pallets/my_pallet, probably it refers to + // ./pallets/my_pallet. We need to transform this path, as otherwise the Cargo.toml won't be + // detected and the pallet won't be added to the workspace. + let pallet_path = prefix_with_current_dir_if_needed(pallet_path); // Determine if the pallet is being created inside a workspace - let workspace_toml = find_workspace_toml(&target); + let workspace_toml = find_workspace_toml(&pallet_path); - let pallet_name = self.name.clone(); - let pallet_path = target.join(pallet_name.clone()); if pallet_path.exists() { if !confirm(format!( "\"{}\" directory already exists. Would you like to remove it?", @@ -55,12 +169,17 @@ impl NewPalletCommand { let spinner = cliclack::spinner(); spinner.start("Generating pallet..."); create_pallet_template( - self.path.clone(), + pallet_path.clone(), TemplatePalletConfig { - name: self.name.clone(), authors: self.authors.clone().expect("default values"), description: self.description.clone().expect("default values"), pallet_in_workspace: workspace_toml.is_some(), + pallet_advanced_mode: self.mode.is_some(), + pallet_default_config, + pallet_common_types, + pallet_storage, + pallet_genesis, + pallet_custom_origin, }, )?; @@ -69,8 +188,20 @@ impl NewPalletCommand { add_crate_to_workspace(&workspace_toml, &pallet_path)?; } + // Format the dir. If this fails we do nothing, it's not a major failure + Command::new("cargo") + .arg("fmt") + .arg("--all") + .current_dir(&pallet_path) + .output()?; + spinner.stop("Generation complete"); - outro(format!("cd into \"{}\" and enjoy hacking! 🚀", &self.name))?; + outro(format!( + "cd into \"{}\" and enjoy hacking! 🚀", + pallet_path + .to_str() + .expect("If the path isn't valid, create_pallet_template detects it; qed") + ))?; Ok(()) } } diff --git a/crates/pop-cli/src/common/helpers.rs b/crates/pop-cli/src/common/helpers.rs new file mode 100644 index 00000000..87e9f1c2 --- /dev/null +++ b/crates/pop-cli/src/common/helpers.rs @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// A macro to facilitate the select multiple variant of an enum and store them inside a `Vec`. +/// # Arguments +/// * `$enum`: The enum type to be iterated over for the selection. This enum must implement +/// `IntoEnumIterator` and `EnumMessage` traits from the `strum` crate. Each variant is +/// responsible of its own messages. +/// * `$prompt_message`: The message displayed to the user. It must implement the `Display` trait. +/// * `$excluded_variants`: If the enum contain variants that shouldn't be included in the +/// multiselect pick, they're specified here. This is useful if a enum is used in a few places and +/// not all of them need all the variants but share some of them. It has to be a `Vec`; +/// # Note +/// This macro only works with a 1-byte sized enums, this is, fieldless enums with at most 255 +/// elements each. This is because we're just interested in letting the user to pick some options +/// among a predefined set, then the name should be descriptive enough, and 1-byte sized enums are +/// really easy to convert to and from a `u8`, so we can work with `u8` all the time and just +/// recover the variant at the end. +/// +/// The decision of using 1-byte enums instead of just fieldless enums is for simplicity: we won't +/// probably offer a user to pick from > 256 options. If this macro is used with enums containing +/// fields, the conversion to `u8` will simply be detected at compile time and the compilation will +/// fail. If this macro is used with fieldless enums greater than 1-byte (really weird but +/// possible), the conversion to u8 will overflow and lead to unexpected behavior, so we panic at +/// runtime if that happens for completeness. +/// +/// # Example +/// +/// ```rust +/// use strum::{IntoEnumIterator, EnumMessage}; +/// use strum_macros::{EnumIter, EnumMessage as EnumMessageDerive}; +/// use cliclack::{multiselect}; +/// use pop_common::multiselect_pick; +/// +/// #[derive(Debug, EnumIter, EnumMessageDerive, Copy, Clone)] +/// enum FieldlessEnum { +/// #[strum(message = "Type 1", detailed_message = "Detailed message for Type 1")] +/// Type1, +/// #[strum(message = "Type 2", detailed_message = "Detailed message for Type 2")] +/// Type2, +/// #[strum(message = "Type 3", detailed_message = "Detailed message for Type 3")] +/// Type3, +/// } +/// +/// fn test_function() -> Result<(),std::io::Error>{ +/// let vec = multiselect_pick!(FieldlessEnum, "Hello, world!"); +/// Ok(()) +/// } +/// ``` +/// +/// # Requirements +/// +/// This macro requires the following imports to function correctly: +/// +/// ```rust +/// use cliclack::{multiselect}; +/// use strum::{EnumMessage, IntoEnumIterator}; +/// ``` +/// +/// Additionally, this macro handle results, so it must be used inside a function doing so. +/// Otherwise the compilation will fail. +#[macro_export] +macro_rules! multiselect_pick { + ($enum: ty, $prompt_message: expr $(, $excluded_variants: expr)?) => {{ + // Ensure the enum is 1-byte long. This is needed cause fieldless enums with > 256 elements + // will lead to unexpected behavior as the conversion to u8 for them isn't detected as wrong + // at compile time. Enums containing variants with fields will be catched at compile time. + // Weird but possible. + assert_eq!(std::mem::size_of::<$enum>(), 1); + let mut prompt = multiselect(format!( + "{} {}", + $prompt_message, + "Pick an option by pressing the spacebar. Press enter when you're done!" + )) + .required(false); + + for variant in <$enum>::iter() { + $(if $excluded_variants.contains(&variant){continue; })? + prompt = prompt.item( + variant as u8, + variant.get_message().unwrap_or_default(), + variant.get_detailed_message().unwrap_or_default(), + ); + } + + // The unsafe block is safe cause the bytes are the discriminants of the enum picked above, + // qed; + prompt + .interact()? + .iter() + .map(|byte| unsafe { std::mem::transmute(*byte) }) + .collect::>() + }}; +} diff --git a/crates/pop-cli/src/common/mod.rs b/crates/pop-cli/src/common/mod.rs index 1f513a16..1cb3ee57 100644 --- a/crates/pop-cli/src/common/mod.rs +++ b/crates/pop-cli/src/common/mod.rs @@ -2,3 +2,4 @@ #[cfg(feature = "contract")] pub mod contracts; +pub mod helpers; diff --git a/crates/pop-common/Cargo.toml b/crates/pop-common/Cargo.toml index 4d826541..8a3afe61 100644 --- a/crates/pop-common/Cargo.toml +++ b/crates/pop-common/Cargo.toml @@ -29,4 +29,4 @@ url.workspace = true [dev-dependencies] mockito.workspace = true strum_macros.workspace = true -tempfile.workspace = true +tempfile.workspace = true \ No newline at end of file diff --git a/crates/pop-common/src/helpers.rs b/crates/pop-common/src/helpers.rs index 08a20369..a63ed854 100644 --- a/crates/pop-common/src/helpers.rs +++ b/crates/pop-common/src/helpers.rs @@ -5,7 +5,7 @@ use std::{ collections::HashMap, fs, io::{Read, Write}, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, }; /// Replaces occurrences of specified strings in a file with new values. @@ -40,6 +40,21 @@ pub fn get_project_name_from_path<'a>(path: &'a Path, default: &'a str) -> &'a s path.file_name().and_then(|name| name.to_str()).unwrap_or(default) } +/// Transforms a path without prefix into a relative path starting at the current directory. +/// +/// # Arguments +/// * `path` - The path to be prefixed if needed. +pub fn prefix_with_current_dir_if_needed(path: PathBuf) -> PathBuf { + let components = &path.components().collect::>(); + if !components.is_empty() { + // If the first component is a normal component, we prefix the path with the current dir + if let Component::Normal(_) = components[0] { + return as AsRef>::as_ref(&Component::CurDir).join(path); + } + } + path +} + #[cfg(test)] mod tests { use super::*; @@ -74,4 +89,31 @@ mod tests { assert_eq!(get_project_name_from_path(path, "my-contract"), "my-contract"); Ok(()) } + + #[test] + fn prefix_with_current_dir_if_needed_works_well() { + let no_prefixed_path = PathBuf::from("my/path".to_string()); + let current_dir_prefixed_path = PathBuf::from("./my/path".to_string()); + let parent_dir_prefixed_path = PathBuf::from("../my/path".to_string()); + let root_dir_prefixed_path = PathBuf::from("/my/path".to_string()); + let empty_path = PathBuf::from("".to_string()); + + assert_eq!( + prefix_with_current_dir_if_needed(no_prefixed_path), + PathBuf::from("./my/path/".to_string()) + ); + assert_eq!( + prefix_with_current_dir_if_needed(current_dir_prefixed_path), + PathBuf::from("./my/path/".to_string()) + ); + assert_eq!( + prefix_with_current_dir_if_needed(parent_dir_prefixed_path), + PathBuf::from("../my/path/".to_string()) + ); + assert_eq!( + prefix_with_current_dir_if_needed(root_dir_prefixed_path), + PathBuf::from("/my/path/".to_string()) + ); + assert_eq!(prefix_with_current_dir_if_needed(empty_path), PathBuf::from("".to_string())); + } } diff --git a/crates/pop-common/src/lib.rs b/crates/pop-common/src/lib.rs index cf375654..d10e792e 100644 --- a/crates/pop-common/src/lib.rs +++ b/crates/pop-common/src/lib.rs @@ -9,7 +9,8 @@ pub mod templates; pub use build::Profile; pub use errors::Error; pub use git::{Git, GitHub, Release}; -pub use helpers::{get_project_name_from_path, replace_in_file}; +pub use helpers::{get_project_name_from_path, prefix_with_current_dir_if_needed, replace_in_file}; +pub use manifest::{add_crate_to_workspace, find_workspace_toml}; pub use templates::extractor::extract_template_files; static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); diff --git a/crates/pop-common/src/manifest.rs b/crates/pop-common/src/manifest.rs index 5ceb31e4..4571d992 100644 --- a/crates/pop-common/src/manifest.rs +++ b/crates/pop-common/src/manifest.rs @@ -7,7 +7,7 @@ use std::{ fs::{read_to_string, write}, path::{Path, PathBuf}, }; -use toml_edit::{value, DocumentMut, Item, Value}; +use toml_edit::{value, Array, DocumentMut, Item, Value}; /// Parses the contents of a `Cargo.toml` manifest. /// @@ -29,14 +29,17 @@ pub fn from_path(path: Option<&Path>) -> Result { Ok(Manifest::from_path(path.canonicalize()?)?) } -/// This function is used to determine if a Path is contained inside a workspace, and returns a -/// PathBuf to the workspace Cargo.toml if found +/// This function is used to determine if a Path is contained inside a workspace, and returns a PathBuf to the workspace Cargo.toml if found. /// /// # Arguments /// * `target_dir` - A directory that may be contained inside a workspace pub fn find_workspace_toml(target_dir: &Path) -> Option { let mut dir = target_dir; while let Some(parent) = dir.parent() { + // This condition is necessary to avoid that calling the function from a workspace using a path which isn't contained in a workspace returns `Some(Cargo.toml)` refering the workspace from where the function has been called instead of the expected `None`. + if parent.to_str() == Some("") { + return None; + } let cargo_toml = parent.join("Cargo.toml"); if cargo_toml.exists() { if let Ok(contents) = read_to_string(&cargo_toml) { @@ -63,25 +66,26 @@ pub fn add_crate_to_workspace(workspace_toml: &Path, crate_path: &Path) -> anyho // Find the relative path to the crate from the workspace root let crate_relative_path = crate_path.strip_prefix(workspace_dir)?; - if let Some(workspace) = doc.get_mut("workspace") { - if let Item::Table(workspace_table) = workspace { - if let Some(members) = workspace_table.get_mut("members") { - if let Item::Value(members_array) = members { - if let Value::Array(array) = members_array { - array.push( - crate_relative_path - .to_str() - .expect("target's always a valid string; qed"), - ); - } else { - return Err(anyhow::anyhow!("Corrupted workspace")); - } + if let Some(Item::Table(workspace_table)) = doc.get_mut("workspace") { + if let Some(Item::Value(members_array)) = workspace_table.get_mut("members") { + if let Value::Array(array) = members_array { + let crate_relative_path = + crate_relative_path.to_str().expect("target's always a valid string; qed"); + let already_in_array = array.iter().any(|member| match member.as_str() { + Some(s) if s == crate_relative_path => true, + _ => false, + }); + if !already_in_array { + array.push(crate_relative_path); } } else { - workspace_table["members"] = value( - crate_relative_path.to_str().expect("target's always a valid string; qed"), - ); + return Err(anyhow::anyhow!("Corrupted workspace")); } + } else { + let mut toml_array = Array::new(); + toml_array + .push(crate_relative_path.to_str().expect("target's always a valid string; qed")); + workspace_table["members"] = value(toml_array); } } else { return Err(anyhow::anyhow!("Corrupted workspace")); @@ -132,26 +136,7 @@ mod tests { } } - fn add_workspace_cargo_toml(self) -> Self { - let workspace_cargo_toml = self - .workspace - .as_ref() - .expect("add_workspace_cargo_toml is only callable if workspace has been created") - .path() - .join("Cargo.toml"); - File::create(&workspace_cargo_toml).expect("Failed to create Cargo.toml"); - write( - &workspace_cargo_toml, - r#"[workspace] - resolver = "2" - members = ["member1"] - "#, - ) - .expect("Failed to write Cargo.toml"); - Self { workspace_cargo_toml: Some(workspace_cargo_toml.to_path_buf()), ..self } - } - - fn add_workspace_cargo_toml_member_not_array(self) -> Self { + fn add_workspace_cargo_toml(self, cargo_toml_content: &str) -> Self { let workspace_cargo_toml = self .workspace .as_ref() @@ -159,26 +144,7 @@ mod tests { .path() .join("Cargo.toml"); File::create(&workspace_cargo_toml).expect("Failed to create Cargo.toml"); - write( - &workspace_cargo_toml, - r#"[workspace] - resolver = "2" - members = "member1" - "#, - ) - .expect("Failed to write Cargo.toml"); - Self { workspace_cargo_toml: Some(workspace_cargo_toml.to_path_buf()), ..self } - } - - fn add_workspace_cargo_toml_not_defining_workspace(self) -> Self { - let workspace_cargo_toml = self - .workspace - .as_ref() - .expect("add_workspace_cargo_toml is only callable if workspace has been created") - .path() - .join("Cargo.toml"); - File::create(&workspace_cargo_toml).expect("Failed to create Cargo.toml"); - write(&workspace_cargo_toml, r#""#).expect("Failed to write Cargo.toml"); + write(&workspace_cargo_toml, cargo_toml_content).expect("Failed to write Cargo.toml"); Self { workspace_cargo_toml: Some(workspace_cargo_toml.to_path_buf()), ..self } } @@ -216,7 +182,12 @@ mod tests { let test_builder = TestBuilder::default() .add_workspace() .add_inside_workspace_dir() - .add_workspace_cargo_toml() + .add_workspace_cargo_toml( + r#"[workspace] + resolver = "2" + members = ["member1"] + "#, + ) .add_outside_workspace_dir(); assert!(find_workspace_toml( test_builder @@ -245,13 +216,20 @@ mod tests { .path() ) .is_none()); + // Calling the function from a relative path which parent is "" returns None + assert!(find_workspace_toml(&PathBuf::from("..")).is_none()); } #[test] - fn add_crate_to_workspace_works_well() { + fn add_crate_to_workspace_works_well_if_members_exists() { let test_builder = TestBuilder::default() .add_workspace() - .add_workspace_cargo_toml() + .add_workspace_cargo_toml( + r#"[workspace] + resolver = "2" + members = ["member1"] + "#, + ) .add_inside_workspace_dir(); let add_crate = add_crate_to_workspace( test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"), @@ -271,6 +249,7 @@ mod tests { if let Some(Item::Value(Value::Array(array))) = workspace_table.get("members") { assert!(array.iter().any(|item| { if let Value::String(item) = item { + // item is only the relative path from the Cargo.toml manifest, while test_buildder.insider_workspace_dir is the absolute path, so we can only test with contains test_builder .inside_workspace_dir .as_ref() @@ -284,10 +263,98 @@ mod tests { } })); } else { - panic!("add_crate_to_workspace fails and should work"); + panic!("This shouldn't be reached"); } } else { - panic!("add_crate_to_workspace fails and should work"); + panic!("This shouldn't be reached"); + } + + // Calling with a crate that's already in the workspace doesn't include it twice + let add_crate = add_crate_to_workspace( + test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"), + test_builder + .inside_workspace_dir + .as_ref() + .expect("Inside workspace dir should exist") + .path(), + ); + assert!(add_crate.is_ok()); + let doc = content.parse::().expect("This should work"); + if let Some(Item::Table(workspace_table)) = doc.get("workspace") { + if let Some(Item::Value(Value::Array(array))) = workspace_table.get("members") { + assert_eq!( + array + .iter() + .filter(|item| { + if let Value::String(item) = item { + test_builder + .inside_workspace_dir + .as_ref() + .expect("Inside workspace should exist") + .path() + .to_str() + .expect("Dir should be mapped to a str") + .contains(item.value()) + } else { + false + } + }) + .count(), + 1 + ); + } else { + panic!("This shouldn't be reached"); + } + } else { + panic!("This shouldn't be reached"); + } + } + + #[test] + fn add_crate_to_workspace_works_well_if_members_doesnt_exist() { + let test_builder = TestBuilder::default() + .add_workspace() + .add_workspace_cargo_toml( + r#"[workspace] + resolver = "2" + "#, + ) + .add_inside_workspace_dir(); + let add_crate = add_crate_to_workspace( + test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"), + test_builder + .inside_workspace_dir + .as_ref() + .expect("Inside workspace dir should exist") + .path(), + ); + assert!(add_crate.is_ok()); + let content = read_to_string( + test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"), + ) + .expect("Cargo.toml should be readable"); + let doc = content.parse::().expect("This should work"); + if let Some(Item::Table(workspace_table)) = doc.get("workspace") { + if let Some(Item::Value(Value::Array(array))) = workspace_table.get("members") { + assert!(array.iter().any(|item| { + if let Value::String(item) = item { + test_builder + .inside_workspace_dir + .as_ref() + .expect("Inside workspace should exist") + .path() + .to_str() + .expect("Dir should be mapped to a str") + .contains(item.value()) + } else { + false + } + })); + } else { + panic!("This shouldn't be reached"); + } + } else { + panic!("This shouldn't be reached"); } } @@ -295,7 +362,12 @@ mod tests { fn add_crate_to_workspace_fails_if_crate_path_not_inside_workspace() { let test_builder = TestBuilder::default() .add_workspace() - .add_workspace_cargo_toml() + .add_workspace_cargo_toml( + r#"[workspace] + resolver = "2" + members = ["member1"] + "#, + ) .add_outside_workspace_dir(); let add_crate = add_crate_to_workspace( test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"), @@ -311,7 +383,12 @@ mod tests { fn add_crate_to_workspace_fails_if_members_not_an_array() { let test_builder = TestBuilder::default() .add_workspace() - .add_workspace_cargo_toml_member_not_array() + .add_workspace_cargo_toml( + r#"[workspace] + resolver = "2" + members = "member1" + "#, + ) .add_inside_workspace_dir(); let add_crate = add_crate_to_workspace( test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"), @@ -327,7 +404,7 @@ mod tests { fn add_crate_to_workspace_fails_if_workspace_isnt_workspace() { let test_builder = TestBuilder::default() .add_workspace() - .add_workspace_cargo_toml_not_defining_workspace() + .add_workspace_cargo_toml(r#""#) .add_inside_workspace_dir(); let add_crate = add_crate_to_workspace( test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"), diff --git a/crates/pop-parachains/Cargo.toml b/crates/pop-parachains/Cargo.toml index f685f63a..1ac8166b 100644 --- a/crates/pop-parachains/Cargo.toml +++ b/crates/pop-parachains/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true [dependencies] anyhow.workspace = true +clap.workspace = true duct.workspace = true flate2.workspace = true glob.workspace = true diff --git a/crates/pop-parachains/README.md b/crates/pop-parachains/README.md index f126ba6e..38fa24c1 100644 --- a/crates/pop-parachains/README.md +++ b/crates/pop-parachains/README.md @@ -122,16 +122,22 @@ Generate a new Pallet: ```rust,no_run use pop_parachains::{create_pallet_template, TemplatePalletConfig}; +use std::path::PathBuf; -let path = "./".to_string(); +let path = "./"; let pallet_config = TemplatePalletConfig { - name: "MyPallet".to_string(), authors: "R0GUE".to_string(), description: "Template pallet".to_string(), - pallet_in_workspace: false + pallet_in_workspace: false, + pallet_advanced_mode: true, + pallet_default_config: true, + pallet_common_types: Vec::new(), + pallet_storage: Vec::new(), + pallet_genesis: false, + pallet_custom_origin: false, }; -create_pallet_template(Some(path),pallet_config); +create_pallet_template(PathBuf::from(path),pallet_config); ``` ## Acknowledgements diff --git a/crates/pop-parachains/src/errors.rs b/crates/pop-parachains/src/errors.rs index 58b59c58..3eff7bbf 100644 --- a/crates/pop-parachains/src/errors.rs +++ b/crates/pop-parachains/src/errors.rs @@ -31,6 +31,8 @@ pub enum Error { OrchestratorError(#[from] OrchestratorError), #[error("Failed to create pallet directory")] PalletDirCreation, + #[error("Invalid path")] + PathError, #[error("Failed to execute rustfmt")] RustfmtError(std::io::Error), #[error("Template error: {0}")] diff --git a/crates/pop-parachains/src/generator/pallet.rs b/crates/pop-parachains/src/generator/pallet.rs index 0d97f252..2c84fde3 100644 --- a/crates/pop-parachains/src/generator/pallet.rs +++ b/crates/pop-parachains/src/generator/pallet.rs @@ -2,40 +2,123 @@ use std::path::PathBuf; -use crate::utils::helpers::write_to_file; +use crate::{ + utils::helpers::write_to_file, TemplatePalletConfigCommonTypes, TemplatePalletStorageTypes, +}; use askama::Template; +mod filters { + /// This filter is used to determine if a element is present in a `Vec` + pub fn contains(vec: &Vec, element: T) -> ::askama::Result { + Ok(vec.contains(&element)) + } +} + #[derive(Template)] #[template(path = "pallet/Cargo.templ", escape = "none")] pub(crate) struct PalletCargoToml { pub(crate) name: String, pub(crate) authors: String, pub(crate) description: String, + // A bool indicating if the pallet has been generated inside a workspace pub(crate) pallet_in_workspace: bool, + // Some common types are used to couple our pallet with a well known one, then adding this type + // here is useful to design Cargo.toml. This pallets should be added as dev-dependencies to + // construct the mock runtime + pub(crate) pallet_common_types: Vec, } + +// Templates for simple mode #[derive(Template)] -#[template(path = "pallet/src/benchmarking.rs.templ", escape = "none")] -pub(crate) struct PalletBenchmarking {} -#[derive(Template)] -#[template(path = "pallet/src/lib.rs.templ", escape = "none")] -pub(crate) struct PalletLib { +#[template(path = "pallet/simple_mode/src/lib.rs.templ", escape = "none")] +pub(crate) struct PalletSimpleLib { pub(crate) name: String, } + #[derive(Template)] -#[template(path = "pallet/src/mock.rs.templ", escape = "none")] -pub(crate) struct PalletMock { +#[template(path = "pallet/simple_mode/src/tests.rs.templ", escape = "none")] +pub(crate) struct PalletSimpleTests { pub(crate) name: String, } + #[derive(Template)] -#[template(path = "pallet/src/tests.rs.templ", escape = "none")] -pub(crate) struct PalletTests { +#[template(path = "pallet/simple_mode/src/mock.rs.templ", escape = "none")] +pub(crate) struct PalletSimpleMock { pub(crate) name: String, } #[derive(Template)] -#[template(path = "pallet/src/weights.rs.templ", escape = "none")] +#[template(path = "pallet/simple_mode/src/benchmarking.rs.templ", escape = "none")] +pub(crate) struct PalletSimpleBenchmarking {} + +#[derive(Template)] +#[template(path = "pallet/simple_mode/src/weights.rs.templ", escape = "none")] pub(crate) struct PalletWeights {} +// Templates for advanced mode +#[derive(Template)] +#[template(path = "pallet/advanced_mode/src/lib.rs.templ", escape = "none")] +pub(crate) struct PalletAdvancedLib { + pub(crate) name: String, + pub(crate) pallet_default_config: bool, + pub(crate) pallet_common_types: Vec, + pub(crate) pallet_storage: Vec, + pub(crate) pallet_genesis: bool, + pub(crate) pallet_custom_origin: bool, +} + +#[derive(Template)] +#[template(path = "pallet/advanced_mode/src/tests.rs.templ", escape = "none")] +pub(crate) struct PalletAdvancedTests {} + +#[derive(Template)] +#[template(path = "pallet/advanced_mode/src/mock.rs.templ", escape = "none")] +pub(crate) struct PalletAdvancedMock { + pub(crate) name: String, + pub(crate) pallet_default_config: bool, + pub(crate) pallet_common_types: Vec, + pub(crate) pallet_custom_origin: bool, +} + +#[derive(Template)] +#[template(path = "pallet/advanced_mode/src/benchmarking.rs.templ", escape = "none")] +pub(crate) struct PalletAdvancedBenchmarking {} + +#[derive(Template)] +#[template(path = "pallet/advanced_mode/src/pallet_logic.rs.templ", escape = "none")] +pub(crate) struct PalletLogic { + pub(crate) pallet_custom_origin: bool, +} + +#[derive(Template)] +#[template(path = "pallet/advanced_mode/src/config_preludes.rs.templ", escape = "none")] +pub(crate) struct PalletConfigPreludes { + pub(crate) pallet_common_types: Vec, + pub(crate) pallet_custom_origin: bool, +} + +#[derive(Template)] +#[template(path = "pallet/advanced_mode/src/pallet_logic/try_state.rs.templ", escape = "none")] +pub(crate) struct PalletTryState {} + +#[derive(Template)] +#[template(path = "pallet/advanced_mode/src/pallet_logic/origin.rs.templ", escape = "none")] +pub(crate) struct PalletOrigin {} + +#[derive(Template)] +#[template(path = "pallet/advanced_mode/src/tests/utils.rs.templ", escape = "none")] +pub(crate) struct PalletTestsUtils { + pub(crate) name: String, +} + +#[derive(Template)] +#[template(path = "pallet/advanced_mode/src/types.rs.templ", escape = "none")] +pub(crate) struct PalletTypes { + pub(crate) pallet_common_types: Vec, + pub(crate) pallet_storage: Vec, + pub(crate) pallet_custom_origin: bool, +} + pub trait PalletItem { /// Render and Write to file, root is the path to the pallet fn execute(&self, root: &PathBuf) -> anyhow::Result<()>; @@ -53,9 +136,19 @@ macro_rules! generate_pallet_item { }; } -generate_pallet_item!(PalletTests, "src/tests.rs"); -generate_pallet_item!(PalletMock, "src/mock.rs"); -generate_pallet_item!(PalletLib, "src/lib.rs"); -generate_pallet_item!(PalletBenchmarking, "src/benchmarking.rs"); generate_pallet_item!(PalletCargoToml, "Cargo.toml"); +generate_pallet_item!(PalletSimpleLib, "src/lib.rs"); +generate_pallet_item!(PalletSimpleTests, "src/tests.rs"); +generate_pallet_item!(PalletSimpleMock, "src/mock.rs"); +generate_pallet_item!(PalletSimpleBenchmarking, "src/benchmarking.rs"); generate_pallet_item!(PalletWeights, "src/weights.rs"); +generate_pallet_item!(PalletAdvancedLib, "src/lib.rs"); +generate_pallet_item!(PalletAdvancedTests, "src/tests.rs"); +generate_pallet_item!(PalletAdvancedMock, "src/mock.rs"); +generate_pallet_item!(PalletAdvancedBenchmarking, "src/benchmarking.rs"); +generate_pallet_item!(PalletLogic, "src/pallet_logic.rs"); +generate_pallet_item!(PalletConfigPreludes, "src/config_preludes.rs"); +generate_pallet_item!(PalletTryState, "src/pallet_logic/try_state.rs"); +generate_pallet_item!(PalletOrigin, "src/pallet_logic/origin.rs"); +generate_pallet_item!(PalletTestsUtils, "src/tests/utils.rs"); +generate_pallet_item!(PalletTypes, "src/types.rs"); diff --git a/crates/pop-parachains/src/lib.rs b/crates/pop-parachains/src/lib.rs index dc23fcc9..06bee8f4 100644 --- a/crates/pop-parachains/src/lib.rs +++ b/crates/pop-parachains/src/lib.rs @@ -16,10 +16,10 @@ pub use build::{ }; pub use errors::Error; pub use indexmap::IndexSet; -pub use new_pallet::{create_pallet_template, TemplatePalletConfig}; +pub use new_pallet::{create_pallet_template, new_pallet_options::*, TemplatePalletConfig}; pub use new_parachain::instantiate_template_dir; pub use templates::{Config, Parachain, Provider}; pub use up::Zombienet; -pub use utils::{helpers::is_initial_endowment_valid, pallet_helpers::resolve_pallet_path}; +pub use utils::helpers::is_initial_endowment_valid; /// Information about the Node. External export from Zombienet-SDK. pub use zombienet_sdk::NetworkNode; diff --git a/crates/pop-parachains/src/new_pallet.rs b/crates/pop-parachains/src/new_pallet.rs index a16c7689..89305ede 100644 --- a/crates/pop-parachains/src/new_pallet.rs +++ b/crates/pop-parachains/src/new_pallet.rs @@ -1,23 +1,45 @@ // SPDX-License-Identifier: GPL-3.0 -use std::{fs, path::PathBuf}; +use std::{ + fs::{create_dir, create_dir_all, File}, + path::PathBuf, +}; + +pub mod new_pallet_options; use crate::{ errors::Error, generator::pallet::{ - PalletBenchmarking, PalletCargoToml, PalletItem, PalletLib, PalletMock, PalletTests, - PalletWeights, + PalletAdvancedBenchmarking, PalletAdvancedLib, PalletAdvancedMock, PalletAdvancedTests, + PalletCargoToml, PalletConfigPreludes, PalletItem, PalletLogic, PalletOrigin, + PalletSimpleBenchmarking, PalletSimpleLib, PalletSimpleMock, PalletSimpleTests, + PalletTestsUtils, PalletTryState, PalletTypes, PalletWeights, }, - resolve_pallet_path, utils::helpers::sanitize, + TemplatePalletConfigCommonTypes, TemplatePalletStorageTypes, }; /// Metadata for the Template Pallet. +#[derive(Debug)] pub struct TemplatePalletConfig { - pub name: String, + /// The authors of the pallet pub authors: String, + /// The pallet description pub description: String, + /// Indicate if the pallet is contained in a workspace pub pallet_in_workspace: bool, + /// Indicate if the user wanna use the advanced mode + pub pallet_advanced_mode: bool, + /// Indicate if the template must include a default config for the pallet. + pub pallet_default_config: bool, + /// Types defined in `TemplatePalletConfigCommonTypes` that should be included in the template. + pub pallet_common_types: Vec, + /// Types defined in `TemplatePalletStorageTypes` that should be included in the template. + pub pallet_storage: Vec, + /// Indicate if the template should include a genesis config + pub pallet_genesis: bool, + /// Indicate if the template should include a custom origin + pub pallet_custom_origin: bool, } /// Create a new pallet from a template. /// @@ -25,111 +47,421 @@ pub struct TemplatePalletConfig { /// /// * `path` - location where the pallet will be created. /// * `config` - customization values to include in the new pallet. -pub fn create_pallet_template( - path: Option, - config: TemplatePalletConfig, -) -> Result<(), Error> { - let target = resolve_pallet_path(path)?; - let pallet_name = config.name.clone(); - let pallet_path = target.join(pallet_name.clone()); - sanitize(&pallet_path)?; - generate_pallet_structure(&target, &pallet_name)?; - - render_pallet(pallet_name, config, &pallet_path)?; +pub fn create_pallet_template(path: PathBuf, config: TemplatePalletConfig) -> Result<(), Error> { + sanitize(&path)?; + generate_pallet_structure(&path, &config)?; + render_pallet(config, &path)?; Ok(()) } -/// Generate a pallet directory and file structure -fn generate_pallet_structure(target: &PathBuf, pallet_name: &str) -> Result<(), Error> { - use fs::{create_dir, File}; - let (pallet, src) = (target.join(pallet_name), target.join(pallet_name.to_string() + "/src")); - create_dir(&pallet)?; +/// Generate a pallet folder and file structure +fn generate_pallet_structure(path: &PathBuf, config: &TemplatePalletConfig) -> Result<(), Error> { + create_dir_all(&path)?; + let (src, pallet_logic, tests) = + (path.join("src"), path.join("src/pallet_logic"), path.join("src/tests")); create_dir(&src)?; - File::create(format!("{}/Cargo.toml", pallet.display()))?; + File::create(format!("{}/Cargo.toml", path.display()))?; File::create(format!("{}/lib.rs", src.display()))?; File::create(format!("{}/benchmarking.rs", src.display()))?; File::create(format!("{}/tests.rs", src.display()))?; File::create(format!("{}/mock.rs", src.display()))?; - File::create(format!("{}/weights.rs", src.display()))?; + if config.pallet_advanced_mode { + create_dir(&pallet_logic)?; + create_dir(&tests)?; + File::create(format!("{}/pallet_logic.rs", src.display()))?; + File::create(format!("{}/try_state.rs", pallet_logic.display()))?; + File::create(format!("{}/types.rs", src.display()))?; + File::create(format!("{}/utils.rs", tests.display()))?; + if config.pallet_default_config { + File::create(format!("{}/config_preludes.rs", src.display()))?; + } + if config.pallet_custom_origin { + File::create(format!("{}/origin.rs", pallet_logic.display()))?; + } + } else { + File::create(format!("{}/weights.rs", src.display()))?; + } Ok(()) } -fn render_pallet( - pallet_name: String, - config: TemplatePalletConfig, - pallet_path: &PathBuf, -) -> Result<(), Error> { - let pallet_name = pallet_name.replace('-', "_"); - // Todo `module` must be of the form Template if pallet_name : `pallet_template` - let pallet: Vec> = vec![ - Box::new(PalletCargoToml { - name: pallet_name.clone(), - authors: config.authors, - description: config.description, - pallet_in_workspace: config.pallet_in_workspace, - }), - Box::new(PalletLib { name: pallet_name.clone() }), - Box::new(PalletBenchmarking {}), - Box::new(PalletMock { name: pallet_name.clone() }), - Box::new(PalletTests { name: pallet_name }), - Box::new(PalletWeights {}), - ]; +fn render_pallet(config: TemplatePalletConfig, pallet_path: &PathBuf) -> Result<(), Error> { + // Extract the pallet name from the path. + let pallet_name = pallet_path + .file_name() + .and_then(|name| name.to_str()) + .ok_or(Error::PathError)? + .replace('-', "_"); + let mut pallet: Vec> = vec![Box::new(PalletCargoToml { + name: pallet_name.clone(), + authors: config.authors, + description: config.description, + pallet_in_workspace: config.pallet_in_workspace, + pallet_common_types: config.pallet_common_types.clone(), + })]; + let mut pallet_contents: Vec>; + if config.pallet_advanced_mode { + pallet_contents = vec![ + Box::new(PalletAdvancedLib { + name: pallet_name.clone(), + pallet_default_config: config.pallet_default_config, + pallet_common_types: config.pallet_common_types.clone(), + pallet_storage: config.pallet_storage.clone(), + pallet_genesis: config.pallet_genesis, + pallet_custom_origin: config.pallet_custom_origin, + }), + Box::new(PalletAdvancedTests {}), + Box::new(PalletAdvancedMock { + name: pallet_name.clone(), + pallet_default_config: config.pallet_default_config, + pallet_common_types: config.pallet_common_types.clone(), + pallet_custom_origin: config.pallet_custom_origin, + }), + Box::new(PalletAdvancedBenchmarking {}), + Box::new(PalletLogic { pallet_custom_origin: config.pallet_custom_origin }), + Box::new(PalletTryState {}), + Box::new(PalletTestsUtils { name: pallet_name.clone() }), + Box::new(PalletTypes { + pallet_common_types: config.pallet_common_types.clone(), + pallet_storage: config.pallet_storage, + pallet_custom_origin: config.pallet_custom_origin, + }), + ]; + if config.pallet_default_config { + pallet_contents.push(Box::new(PalletConfigPreludes { + pallet_common_types: config.pallet_common_types, + pallet_custom_origin: config.pallet_custom_origin, + })); + } + + if config.pallet_custom_origin { + pallet_contents.push(Box::new(PalletOrigin {})); + } + } else { + pallet_contents = vec![ + Box::new(PalletSimpleLib { name: pallet_name.clone() }), + Box::new(PalletSimpleTests { name: pallet_name.clone() }), + Box::new(PalletSimpleMock { name: pallet_name.clone() }), + Box::new(PalletSimpleBenchmarking {}), + Box::new(PalletWeights {}), + ]; + } + + pallet.extend(pallet_contents); + for item in pallet { item.execute(pallet_path)?; } + Ok(()) } #[cfg(test)] mod tests { use super::*; + use std::fs::read_to_string; #[test] - fn test_pallet_create_template() -> Result<(), Error> { + fn test_pallet_create_advanced_template() -> Result<(), Error> { let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); let pallet_name = "MyPallet"; + let pallet_path = temp_dir.path().join(pallet_name); let config = TemplatePalletConfig { - name: pallet_name.to_string(), authors: "Alice".to_string(), description: "A sample pallet".to_string(), pallet_in_workspace: false, + pallet_advanced_mode: true, + pallet_default_config: true, + pallet_common_types: Vec::new(), + pallet_storage: Vec::new(), + pallet_genesis: false, + pallet_custom_origin: true, }; // Call the function being tested - create_pallet_template(Some(temp_dir.path().to_str().unwrap().to_string()), config)?; + create_pallet_template(pallet_path.clone(), config)?; // Assert that the pallet structure is generated + assert!(pallet_path.exists(), "Pallet folder should be created"); + assert!(pallet_path.join("src").exists(), "src folder should be created"); + assert!( + pallet_path.join("src").join("pallet_logic").exists(), + "pallet_logic folder should be created" + ); + assert!( + pallet_path.join("src").join("pallet_logic").join("try_state.rs").exists(), + "try_state.rs should be created" + ); + assert!( + pallet_path.join("src").join("pallet_logic").join("origin.rs").exists(), + "origin.rs should be created" + ); + assert!(pallet_path.join("src").join("tests").exists(), "tests folder should be created"); + assert!( + pallet_path.join("src").join("tests").join("utils.rs").exists(), + "utils.rs folder should be created" + ); + assert!(pallet_path.join("Cargo.toml").exists(), "Cargo.toml should be created"); + assert!(pallet_path.join("src").join("lib.rs").exists(), "lib.rs should be created"); + assert!( + pallet_path.join("src").join("benchmarking.rs").exists(), + "benchmarking.rs should be created" + ); + assert!(pallet_path.join("src").join("tests.rs").exists(), "tests.rs should be created"); + assert!( + !pallet_path.join("src").join("weights.rs").exists(), + "weights.rs shouldn't be created" + ); + assert!(pallet_path.join("src").join("mock.rs").exists(), "mock.rs should be created"); + assert!( + pallet_path.join("src").join("pallet_logic.rs").exists(), + "pallet_logic.rs should be created" + ); + assert!( + pallet_path.join("src").join("config_preludes.rs").exists(), + "config_preludes.rs should be created" + ); + + let lib_rs_content = + read_to_string(pallet_path.join("src").join("lib.rs")).expect("Failed to read lib.rs"); + assert!(lib_rs_content.contains("pub mod pallet"), "lib.rs should contain pub mod pallet"); + assert!( + lib_rs_content.contains("pub mod config_preludes"), + "lib.rs should contain pub mod config_preludes" + ); + Ok(()) + } + + #[test] + fn test_pallet_create_advanced_template_no_default_config() -> Result<(), Error> { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let pallet_name = "MyPallet"; let pallet_path = temp_dir.path().join(pallet_name); - assert!(pallet_path.exists(), "Pallet directory should be created"); - assert!(pallet_path.join("src").exists(), "src directory should be created"); + let config = TemplatePalletConfig { + authors: "Alice".to_string(), + description: "A sample pallet".to_string(), + pallet_in_workspace: false, + pallet_advanced_mode: true, + pallet_default_config: false, + pallet_common_types: Vec::new(), + pallet_storage: Vec::new(), + pallet_genesis: false, + pallet_custom_origin: true, + }; + + // Call the function being tested + create_pallet_template(pallet_path.clone(), config)?; + + // Assert that the pallet structure is generated + assert!(pallet_path.exists(), "Pallet folder should be created"); + assert!(pallet_path.join("src").exists(), "src folder should be created"); + assert!( + pallet_path.join("src").join("pallet_logic").exists(), + "pallet_logic folder should be created" + ); + assert!( + pallet_path.join("src").join("pallet_logic").join("try_state.rs").exists(), + "try_state.rs should be created" + ); + assert!( + pallet_path.join("src").join("pallet_logic").join("origin.rs").exists(), + "origin.rs should be created" + ); + assert!(pallet_path.join("src").join("tests").exists(), "tests folder should be created"); + assert!( + pallet_path.join("src").join("tests").join("utils.rs").exists(), + "utils.rs folder should be created" + ); assert!(pallet_path.join("Cargo.toml").exists(), "Cargo.toml should be created"); assert!(pallet_path.join("src").join("lib.rs").exists(), "lib.rs should be created"); assert!( pallet_path.join("src").join("benchmarking.rs").exists(), "benchmarking.rs should be created" ); + assert!( + !pallet_path.join("src").join("weights.rs").exists(), + "weights.rs shouldn't be created" + ); assert!(pallet_path.join("src").join("tests.rs").exists(), "tests.rs should be created"); assert!(pallet_path.join("src").join("mock.rs").exists(), "mock.rs should be created"); + assert!( + pallet_path.join("src").join("pallet_logic.rs").exists(), + "pallet_logic.rs should be created" + ); + assert!( + !pallet_path.join("src").join("config_preludes.rs").exists(), + "config_preludes.rs should be created" + ); - let lib_rs_content = fs::read_to_string(pallet_path.join("src").join("lib.rs")) - .expect("Failed to read lib.rs"); + let lib_rs_content = + read_to_string(pallet_path.join("src").join("lib.rs")).expect("Failed to read lib.rs"); assert!(lib_rs_content.contains("pub mod pallet"), "lib.rs should contain pub mod pallet"); + assert!( + !lib_rs_content.contains("pub mod config_preludes"), + "lib.rs should contain pub mod config_preludes" + ); + Ok(()) + } + + #[test] + fn test_pallet_create_advanced_template_no_custom_origin() -> Result<(), Error> { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let pallet_name = "MyPallet"; + let pallet_path = temp_dir.path().join(pallet_name); + let config = TemplatePalletConfig { + authors: "Alice".to_string(), + description: "A sample pallet".to_string(), + pallet_in_workspace: true, + pallet_advanced_mode: true, + pallet_default_config: true, + pallet_common_types: Vec::new(), + pallet_storage: Vec::new(), + pallet_genesis: false, + pallet_custom_origin: false, + }; + + // Call the function being tested + create_pallet_template(pallet_path.clone(), config)?; + + // Assert that the pallet structure is generated + assert!(pallet_path.exists(), "Pallet folder should be created"); + assert!(pallet_path.join("src").exists(), "src folder should be created"); + assert!( + pallet_path.join("src").join("pallet_logic").exists(), + "pallet_logic folder should be created" + ); + assert!( + pallet_path.join("src").join("pallet_logic").join("try_state.rs").exists(), + "try_state.rs should be created" + ); + assert!( + !pallet_path.join("src").join("pallet_logic").join("origin.rs").exists(), + "origin.rs should be created" + ); + assert!(pallet_path.join("src").join("tests").exists(), "tests folder should be created"); + assert!( + pallet_path.join("src").join("tests").join("utils.rs").exists(), + "utils.rs folder should be created" + ); + assert!(pallet_path.join("Cargo.toml").exists(), "Cargo.toml should be created"); + assert!(pallet_path.join("src").join("lib.rs").exists(), "lib.rs should be created"); + assert!( + pallet_path.join("src").join("benchmarking.rs").exists(), + "benchmarking.rs should be created" + ); + assert!( + !pallet_path.join("src").join("weights.rs").exists(), + "weights.rs shouldn't be created" + ); + assert!(pallet_path.join("src").join("tests.rs").exists(), "tests.rs should be created"); + assert!(pallet_path.join("src").join("mock.rs").exists(), "mock.rs should be created"); + assert!( + pallet_path.join("src").join("pallet_logic.rs").exists(), + "pallet_logic.rs should be created" + ); + assert!( + pallet_path.join("src").join("config_preludes.rs").exists(), + "config_preludes.rs should be created" + ); + + let lib_rs_content = + read_to_string(pallet_path.join("src").join("lib.rs")).expect("Failed to read lib.rs"); + assert!(lib_rs_content.contains("pub mod pallet"), "lib.rs should contain pub mod pallet"); + assert!( + lib_rs_content.contains("pub mod config_preludes"), + "lib.rs should contain pub mod config_preludes" + ); + Ok(()) + } + + #[test] + fn test_pallet_create_simple_template() -> Result<(), Error> { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let pallet_name = "MyPallet"; + let pallet_path = temp_dir.path().join(pallet_name); + let config = TemplatePalletConfig { + authors: "Alice".to_string(), + description: "A sample pallet".to_string(), + pallet_in_workspace: false, + pallet_advanced_mode: false, + pallet_default_config: false, + pallet_common_types: Vec::new(), + pallet_storage: Vec::new(), + pallet_genesis: false, + pallet_custom_origin: false, + }; + + // Call the function being tested + create_pallet_template(pallet_path.clone(), config)?; + + // Assert that the pallet structure is generated + let pallet_path = temp_dir.path().join(pallet_name); + assert!(pallet_path.exists(), "Pallet folder should be created"); + assert!(pallet_path.join("src").exists(), "src folder should be created"); + assert!( + !pallet_path.join("src").join("pallet_logic").exists(), + "pallet_logic folder shouldn't be created" + ); + assert!( + !pallet_path.join("src").join("pallet_logic").join("try_state.rs").exists(), + "try_state.rs shouldn't be created" + ); + assert!( + !pallet_path.join("src").join("pallet_logic").join("origin.rs").exists(), + "origin.rs shouldn't be created" + ); + assert!(!pallet_path.join("src").join("tests").exists(), "tests folder should be created"); + assert!( + !pallet_path.join("src").join("tests").join("utils.rs").exists(), + "utils.rs folder shouldn't be created" + ); + assert!(pallet_path.join("Cargo.toml").exists(), "Cargo.toml should be created"); + assert!(pallet_path.join("src").join("lib.rs").exists(), "lib.rs should be created"); + assert!( + pallet_path.join("src").join("benchmarking.rs").exists(), + "benchmarking.rs should be created" + ); + assert!( + pallet_path.join("src").join("weights.rs").exists(), + "weights.rs should be created" + ); + assert!(pallet_path.join("src").join("tests.rs").exists(), "tests.rs should be created"); + assert!(pallet_path.join("src").join("mock.rs").exists(), "mock.rs should be created"); + assert!( + !pallet_path.join("src").join("pallet_logic.rs").exists(), + "pallet_logic.rs shouldn't be created" + ); + assert!( + !pallet_path.join("src").join("config_preludes.rs").exists(), + "config_preludes.rs shouldn't be created" + ); + + let lib_rs_content = + read_to_string(pallet_path.join("src").join("lib.rs")).expect("Failed to read lib.rs"); + assert!(lib_rs_content.contains("pub mod pallet"), "lib.rs should contain pub mod pallet"); + assert!( + !lib_rs_content.contains("pub mod config_preludes"), + "lib.rs shouldn't contain pub mod config_preludes" + ); Ok(()) } #[test] fn test_pallet_create_template_invalid_path() { let invalid_path = "/invalid/path/that/does/not/exist"; - let pallet_name = "MyPallet"; let config = TemplatePalletConfig { - name: pallet_name.to_string(), authors: "Alice".to_string(), description: "A sample pallet".to_string(), pallet_in_workspace: false, + pallet_advanced_mode: true, + pallet_default_config: false, + pallet_common_types: Vec::new(), + pallet_storage: Vec::new(), + pallet_genesis: false, + pallet_custom_origin: false, }; // Call the function being tested with an invalid path - let result = create_pallet_template(Some(invalid_path.to_string()), config); + let result = create_pallet_template(PathBuf::from(invalid_path), config); // Assert that the result is an error assert!(result.is_err(), "Result should be an error"); diff --git a/crates/pop-parachains/src/new_pallet/new_pallet_options.rs b/crates/pop-parachains/src/new_pallet/new_pallet_options.rs new file mode 100644 index 00000000..d0109a3d --- /dev/null +++ b/crates/pop-parachains/src/new_pallet/new_pallet_options.rs @@ -0,0 +1,87 @@ +use clap::ValueEnum; +use strum_macros::{EnumIter, EnumMessage}; + +/// This enum is used to register from the CLI which types that are kind of usual in config traits +/// are included in the pallet +#[derive(Debug, Copy, Clone, PartialEq, EnumIter, EnumMessage, ValueEnum)] +pub enum TemplatePalletConfigCommonTypes { + /// This type will enable your pallet to emit events. + #[strum( + message = "RuntimeEvent", + detailed_message = "This type will enable your pallet to emit events." + )] + RuntimeEvent, + /// This type will be helpful if your pallet needs to deal with the outer RuntimeOrigin enum, + /// or if your pallet needs to use custom origins. Note: If you have run the command using -o, + /// this type will be added anyway. + #[strum( + message = "RuntimeOrigin", + detailed_message = "This type will be helpful if your pallet needs to deal with the outer RuntimeOrigin enum, or if your pallet needs to use custom origins. Note: If you have run the command using -o, this type will be added anyway." + )] + RuntimeOrigin, + /// This type will allow your pallet to interact with the native currency of the blockchain. + #[strum( + message = "Currency", + detailed_message = "This type will allow your pallet to interact with the native currency of the blockchain." + )] + Currency, +} + +/// This enum is used to determine which storage shape has a storage item in the pallet +#[derive(Debug, Copy, Clone, PartialEq, EnumIter, EnumMessage, ValueEnum)] +pub enum TemplatePalletStorageTypes { + /// A storage value is a single value of a given type stored on-chain. + #[strum( + message = "StorageValue", + detailed_message = "A storage value is a single value of a given type stored on-chain." + )] + StorageValue, + /// A storage map is a mapping of keys to values of a given type stored on-chain. + #[strum( + message = "StorageMap", + detailed_message = "A storage map is a mapping of keys to values of a given type stored on-chain." + )] + StorageMap, + /// A wrapper around a StorageMap and a StorageValue (with the value being u32) to keep track + /// of how many items are in a map. + #[strum( + message = "CountedStorageMap", + detailed_message = "A wrapper around a StorageMap and a StorageValue (with the value being u32) to keep track of how many items are in a map." + )] + CountedStorageMap, + /// This structure associates a pair of keys with a value of a specified type stored on-chain. + #[strum( + message = "StorageDoubleMap", + detailed_message = "This structure associates a pair of keys with a value of a specified type stored on-chain." + )] + StorageDoubleMap, + /// This structure associates an arbitrary number of keys with a value of a specified type + /// stored on-chain. + #[strum( + message = "StorageNMap", + detailed_message = "This structure associates an arbitrary number of keys with a value of a specified type stored on-chain." + )] + StorageNMap, + /// A wrapper around a StorageNMap and a StorageValue (with the value being u32) to keep track + /// of how many items are in a map. + #[strum( + message = "CountedStorageNMap", + detailed_message = "A wrapper around a StorageNMap and a StorageValue (with the value being u32) to keep track of how many items are in a map." + )] + CountedStorageNMap, +} + +/// This enum is used to register from the CLI which options are selected by the user to be included +/// in the pallet. +#[derive(Debug, Copy, Clone, PartialEq, EnumIter, EnumMessage)] +pub enum TemplatePalletOptions { + #[strum( + message = "DefaultConfig", + detailed_message = "Use a default configuration for your config trait." + )] + DefaultConfig, + #[strum(message = "GenesisConfig", detailed_message = "Add a genesis config to your pallet.")] + GenesisConfig, + #[strum(message = "Custom Origin", detailed_message = "Add a custom origin to your pallet.")] + CustomOrigin, +} diff --git a/crates/pop-parachains/src/utils/mod.rs b/crates/pop-parachains/src/utils/mod.rs index e5b742ea..265ebafd 100644 --- a/crates/pop-parachains/src/utils/mod.rs +++ b/crates/pop-parachains/src/utils/mod.rs @@ -1,4 +1,3 @@ // SPDX-License-Identifier: GPL-3.0 pub mod helpers; -pub mod pallet_helpers; diff --git a/crates/pop-parachains/src/utils/pallet_helpers.rs b/crates/pop-parachains/src/utils/pallet_helpers.rs deleted file mode 100644 index 48c3b0f1..00000000 --- a/crates/pop-parachains/src/utils/pallet_helpers.rs +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -use crate::errors::Error; -use std::{ - env::current_dir, - fs, - path::{Path, PathBuf}, - process, -}; - -/// Resolve pallet path -/// For a template it should be `