Add init command

This commit is contained in:
Moritz Hölting 2024-03-27 13:58:09 +01:00
commit 25890fc392
11 changed files with 286 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "shulkerscript"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
colored = "2.1.0"
serde = { version = "1.0.197", features = ["derive"] }
thiserror = "1.0.58"
toml = "0.8.12"

59
src/cli.rs Normal file
View File

@ -0,0 +1,59 @@
use std::path::PathBuf;
use crate::{error::Result, subcommands};
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Args {
#[command(subcommand)]
cmd: Command,
/// Enable verbose output.
#[clap(short, long)]
verbose: bool,
}
#[derive(Debug, Clone, Subcommand)]
pub enum Command {
/// Initialize a new project in the current directory.
Init {
/// The path of the folder to initialize in.
#[clap(default_value = ".")]
path: PathBuf,
/// The name of the project.
#[clap(short, long)]
name: Option<String>,
/// The description of the project.
#[clap(short, long)]
description: Option<String>,
/// The pack format version.
#[clap(short, long)]
pack_format: Option<u8>,
/// Force initialization even if the directory is not empty.
#[clap(short, long)]
force: bool,
},
}
impl Args {
pub fn run(&self) -> Result<()> {
match &self.cmd {
Command::Init {
path,
name,
description,
pack_format,
force,
} => subcommands::init(
self.verbose,
path,
name.as_deref(),
description.as_deref(),
*pack_format,
*force,
)?,
}
Ok(())
}
}

49
src/config.rs Normal file
View File

@ -0,0 +1,49 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProjectConfig {
pub pack: PackConfig,
}
impl ProjectConfig {
pub fn new(pack: PackConfig) -> Self {
Self { pack }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackConfig {
pub name: String,
pub description: String,
#[serde(
rename = "format",
alias = "pack_format",
default = "default_pack_format"
)]
pub pack_format: u8,
pub version: String,
}
impl PackConfig {
pub fn new(name: &str, description: &str, pack_format: u8) -> Self {
Self {
name: name.to_string(),
description: description.to_string(),
pack_format,
version: "0.1.0".to_string(),
}
}
}
impl Default for PackConfig {
fn default() -> Self {
Self {
name: "shulkerscript-pack".to_string(),
description: "A Minecraft datapack created with shulkerscript".to_string(),
pack_format: 26,
version: "0.1.0".to_string(),
}
}
}
fn default_pack_format() -> u8 {
26
}

19
src/error.rs Normal file
View File

@ -0,0 +1,19 @@
use std::path::PathBuf;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("An error occurred while parsing command-line arguments.")]
IoError(#[from] std::io::Error),
#[error("An error occured while serializing to TOML.")]
TomlSerializeError(#[from] toml::ser::Error),
#[error("An error occured while deserializing from TOML.")]
TomlDeserializeError(#[from] toml::de::Error),
#[error("No file/directory found at path {0}.")]
PathNotFoundError(PathBuf),
#[error("An error occured because the directory {0} is not empty.")]
NonEmptyDirectoryError(PathBuf),
#[error("An error occured because the path {0} is not a directory.")]
NotDirectoryError(PathBuf),
}
pub type Result<T> = std::result::Result<T, Error>;

6
src/lib.rs Normal file
View File

@ -0,0 +1,6 @@
pub mod cli;
pub mod config;
pub mod error;
pub mod subcommands;
pub mod terminal_output;
pub mod util;

13
src/main.rs Normal file
View File

@ -0,0 +1,13 @@
use std::process::ExitCode;
use clap::Parser;
use shulkerscript::cli::Args;
fn main() -> ExitCode {
let args = Args::parse();
match args.run() {
Ok(_) => ExitCode::SUCCESS,
Err(_) => ExitCode::FAILURE,
}
}

103
src/subcommands/init.rs Normal file
View File

@ -0,0 +1,103 @@
use std::{fs, path::Path};
use crate::{
config::ProjectConfig,
error::{Error, Result},
terminal_output::{print_error, print_info, print_success},
util::to_absolute_path,
};
pub fn init(
verbose: bool,
path: &Path,
name: Option<&str>,
description: Option<&str>,
pack_format: Option<u8>,
force: bool,
) -> Result<()> {
if !path.exists() {
print_error("The specified path does not exist.");
Err(Error::PathNotFoundError(path.to_path_buf()))
} else if !path.is_dir() {
print_error("The specified path is not a directory.");
Err(Error::NotDirectoryError(path.to_path_buf()))
} else if !force && path.read_dir()?.next().is_some() {
print_error("The specified directory is not empty.");
Err(Error::NonEmptyDirectoryError(path.to_path_buf()))
} else {
print_info("Initializing a new Shulkerscript project...");
// Create the pack.toml file
create_pack_config(verbose, path, name, description, pack_format)?;
// Create the .gitignore file
create_gitignore(path, verbose)?;
// Create the src directory
let src_path = path.join("src");
create_dir(&src_path, verbose)?;
print_success("Project initialized successfully.");
Ok(())
}
}
fn create_pack_config(
verbose: bool,
base_path: &Path,
name: Option<&str>,
description: Option<&str>,
pack_format: Option<u8>,
) -> Result<()> {
let pack_name = name.or_else(|| base_path.file_name().and_then(|os| os.to_str()));
let path = base_path.join("pack.toml");
// Load the default config
let mut content = ProjectConfig::default();
// Override the default values with the provided ones
if let Some(name) = pack_name {
content.pack.name = name.to_string();
}
if let Some(description) = description {
content.pack.description = description.to_string();
}
if let Some(pack_format) = pack_format {
content.pack.pack_format = pack_format;
}
fs::write(&path, toml::to_string_pretty(&content)?)?;
if verbose {
print_info(&format!(
"Created pack.toml file at {}.",
to_absolute_path(&path)?
));
}
Ok(())
}
fn create_dir(path: &Path, verbose: bool) -> std::io::Result<()> {
if !path.exists() {
fs::create_dir(path)?;
if verbose {
print_info(&format!(
"Created directory at {}.",
to_absolute_path(path)?
));
}
}
Ok(())
}
fn create_gitignore(path: &Path, verbose: bool) -> std::io::Result<()> {
let gitignore = path.join(".gitignore");
fs::write(&gitignore, "/dist\n")?;
if verbose {
print_info(&format!(
"Created .gitignore file at {}.",
to_absolute_path(&gitignore)?
));
}
Ok(())
}

2
src/subcommands/mod.rs Normal file
View File

@ -0,0 +1,2 @@
mod init;
pub use init::init;

12
src/terminal_output.rs Normal file
View File

@ -0,0 +1,12 @@
use colored::Colorize;
pub fn print_info(msg: &str) {
println!("[{}] {msg}", "INFO".blue())
}
pub fn print_success(msg: &str) {
println!("[{}] {msg}", "SUCCESS".green())
}
pub fn print_error(msg: &str) {
println!("[{}] {msg}", "ERROR".red())
}

9
src/util.rs Normal file
View File

@ -0,0 +1,9 @@
use std::{io, path::Path};
pub fn to_absolute_path(path: &Path) -> io::Result<String> {
Ok(std::fs::canonicalize(path)?
.display()
.to_string()
.trim_start_matches(r"\\?\")
.to_string())
}