Skip to content

Commit

Permalink
feat: improve pallet template generation (#261)
Browse files Browse the repository at this point in the history
* Interactive interface added

* Update pick_options_and_give_name macro to handle an arbitrary amount of cases, allowing pick several options for the same object

* New macro for multiselect from an enum. Improve the CLI to support more options

* Adding the possibility of using custom origins

* Adding Cargo.toml generation

* Template lib.rs. Pending of review

* Finishing lib (and related) templates

* New mock config

* Finishing templates

* Adding some tests for the folder structure

* Formatting repo

* Solving mock implementation of common types when no default config issue

* Adding more links to Polkadot-SDK-docs

* Formatting lib.rs.templ

* Solving small issues

* Solving doc test issue, the struct used in that test was modified by this PR

* Apply suggestions from code review

Comment corrections by Bruno

Co-authored-by: Bruno Galvao <brunopgalvao@gmail.com>

* Adding simple/advanced modes

* Adding advanced mode

* Adding advanced mode

* Formatting and solving conflicts

* Solving small template issues

* Formatting the template after generation

* Formatting pallet

* Updating template generation

* Improving manifest test suite

* Allowing storage/common_types from CLI + template improvement

* Solving bug related to PR #277: Regenerating a crate with POP that's already in the workspace doesn't include it again

* Finishing templates with examples; solving some small bugs produced when a pallet is generated from a workspace

* Finishing templates with examples; solving some small bugs produced when a pallet is generated from a workspace

* Deleting legacy file

* Improving UX

* Applying suggested changes

* Applying suggested changes

* Adding a whiteline to lib.rs.templ for formatting, not so important

* Not including default config if config trait empty

* Improve advanced help examples

* Improve advanced help examples

* Improve advanced help examples

* Applying suggestions

---------

Co-authored-by: Bruno Galvao <brunopgalvao@gmail.com>
  • Loading branch information
tsenovilla and brunopgalvao authored Aug 30, 2024
1 parent e829471 commit 43b592b
Show file tree
Hide file tree
Showing 33 changed files with 1,673 additions and 219 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

181 changes: 156 additions & 25 deletions crates/pop-cli/src/commands/new/pallet.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
#[arg(short, long, help = "Name of authors", default_value = "Anonymous")]
pub(crate) authors: Option<String>,
#[arg(short, long, help = "Pallet description", default_value = "Frame Pallet")]
pub(crate) description: Option<String>,
#[arg(short = 'p', long, help = "Path to the pallet, [default: current directory]")]
pub(crate) path: Option<String>,
#[command(subcommand)]
pub(crate) mode: Option<Mode>,
}

#[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<TemplatePalletConfigCommonTypes>,
#[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<TemplatePalletStorageTypes>,
#[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?",
Expand All @@ -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,
},
)?;

Expand All @@ -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(())
}
}
93 changes: 93 additions & 0 deletions crates/pop-cli/src/common/helpers.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<$enum>>()
}};
}
1 change: 1 addition & 0 deletions crates/pop-cli/src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

#[cfg(feature = "contract")]
pub mod contracts;
pub mod helpers;
2 changes: 1 addition & 1 deletion crates/pop-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ url.workspace = true
[dev-dependencies]
mockito.workspace = true
strum_macros.workspace = true
tempfile.workspace = true
tempfile.workspace = true
44 changes: 43 additions & 1 deletion crates/pop-common/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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::<Vec<Component>>();
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 <Component<'_> as AsRef<Path>>::as_ref(&Component::CurDir).join(path);
}
}
path
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -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()));
}
}
Loading

0 comments on commit 43b592b

Please sign in to comment.