diff --git a/crates/common/src/api/papi.rs b/crates/common/src/api/papi.rs index cb76a4c0..e08f3f55 100644 --- a/crates/common/src/api/papi.rs +++ b/crates/common/src/api/papi.rs @@ -3,7 +3,7 @@ use std::fs::File; use self::client::{ApiError, ApiErrorKind, ApiResult, HandleResponse}; use crate::function::{ CreateFunctionResponse, Function, FunctionDeployment, FunctionDeploymentCredentials, - FunctionDeploymentStatus, GetFunctionResponse, + GetFunctionResponse, }; use crate::relay::{CreateRelay, Relay}; use serde_json::json; diff --git a/crates/ev-cli/src/commands/function/create_toml.rs b/crates/ev-cli/src/commands/function/create_toml.rs new file mode 100644 index 00000000..b134e920 --- /dev/null +++ b/crates/ev-cli/src/commands/function/create_toml.rs @@ -0,0 +1,125 @@ +use std::{fs, path::PathBuf, str::FromStr}; + +use clap::Parser; +use serde::Serialize; +use thiserror::Error; + +use crate::{ + commands::interact::{input, preset_input, select, validated_input, validators}, + CmdOutput, +}; + +#[derive(Serialize)] +struct FunctionConfig { + name: String, + language: String, + handler: String, +} + +#[derive(Serialize)] +struct FunctionToml { + function: FunctionConfig, +} + +/// Generate a toml configuration file for your Function +#[derive(Parser, Debug)] +pub struct CreateTomlArgs {} + +#[derive(strum_macros::Display, Debug)] +pub enum CreateTomlPrompt { + #[strum(to_string = "Give your Function a name:")] + Name, + #[strum(to_string = "Select your Function's language:")] + Language, + #[strum(to_string = "What is the entry point to your function?:")] + Handler, +} + +#[derive(strum_macros::Display, Debug)] +pub enum CreateTomlMessage { + #[strum(to_string = "Function configuration saved to function.toml.")] + Success, +} + +impl CmdOutput for CreateTomlMessage { + fn exitcode(&self) -> crate::errors::ExitCode { + crate::errors::OK + } + + fn code(&self) -> String { + match self { + CreateTomlMessage::Success => "function-create-toml-success", + } + .to_string() + } +} + +#[derive(Error, Debug)] +pub enum CreateTomlError { + #[error("An IO error occurred: {0}")] + Io(#[from] std::io::Error), + #[error("A function.toml file already exists in the current directory")] + AlreadyExists, +} + +impl CmdOutput for CreateTomlError { + fn exitcode(&self) -> crate::errors::ExitCode { + match self { + CreateTomlError::Io(_) => crate::errors::IOERR, + CreateTomlError::AlreadyExists => crate::errors::SOFTWARE, + } + } + + fn code(&self) -> String { + match self { + CreateTomlError::Io(_) => "function-create-toml-io-error", + CreateTomlError::AlreadyExists => "function-create-toml-already-exists", + } + .to_string() + } +} + +pub async fn run(_: CreateTomlArgs) -> Result { + if PathBuf::from_str("./function.toml") + .expect("infallible") + .exists() + { + return Err(CreateTomlError::AlreadyExists); + } + + let valid_languages: [&str; 5] = [ + "node@18", + "node@20", + "python@3.9", + "python@3.10", + "python@3.11", + ]; + + let name = validated_input( + CreateTomlPrompt::Name, + false, + Box::new(validators::validate_function_name), + )?; + + let langs = valid_languages + .iter() + .map(|lang| lang.to_string()) + .collect::>(); + + let language = select(&langs, 0, CreateTomlPrompt::Language).unwrap(); + + let handler = preset_input(CreateTomlPrompt::Handler, "index.handler".to_string()).unwrap(); + + let config = FunctionToml { + function: FunctionConfig { + name, + language: valid_languages[language].to_string(), + handler: handler.to_string(), + }, + }; + + let toml = toml::to_string(&config).unwrap(); + fs::write("function.toml", toml)?; + + return Ok(CreateTomlMessage::Success); +} diff --git a/crates/ev-cli/src/commands/function/init.rs b/crates/ev-cli/src/commands/function/init.rs index 62aa1851..5e95bec9 100644 --- a/crates/ev-cli/src/commands/function/init.rs +++ b/crates/ev-cli/src/commands/function/init.rs @@ -77,6 +77,8 @@ impl CmdOutput for InitMessage { pub enum InitPrompt { #[strum(to_string = "Give your function a name:")] Name, + #[strum(to_string = "Select your function's language:")] + Language, } pub async fn run(args: InitArgs, auth: BasicAuth) -> Result { @@ -90,12 +92,7 @@ pub async fn run(args: InitArgs, auth: BasicAuth) -> Result>(); - let language = interact::select( - &langs, - 0, - Some("Select your Function's language:".to_string()), - ) - .unwrap(); + let language = interact::select(&langs, 0, InitPrompt::Language).unwrap(); let lang = valid_languages[language].to_string(); let file = api_client.get_hello_function_template(lang.clone()).await?; diff --git a/crates/ev-cli/src/commands/function/mod.rs b/crates/ev-cli/src/commands/function/mod.rs index 2aee10da..7a3ea1df 100644 --- a/crates/ev-cli/src/commands/function/mod.rs +++ b/crates/ev-cli/src/commands/function/mod.rs @@ -1,6 +1,7 @@ use crate::run_cmd; use clap::Parser; +mod create_toml; mod deploy; mod init; @@ -16,6 +17,7 @@ pub struct FunctionArgs { pub enum FunctionCommand { Init(init::InitArgs), Deploy(deploy::DeployArgs), + CreateToml(create_toml::CreateTomlArgs), } pub async fn run(args: FunctionArgs) { @@ -24,5 +26,8 @@ pub async fn run(args: FunctionArgs) { match args.action { FunctionCommand::Init(init_args) => run_cmd(init::run(init_args, auth).await), FunctionCommand::Deploy(deploy_args) => run_cmd(deploy::run(deploy_args, auth).await), + FunctionCommand::CreateToml(create_toml_args) => { + run_cmd(create_toml::run(create_toml_args).await) + } } } diff --git a/crates/ev-cli/src/commands/interact.rs b/crates/ev-cli/src/commands/interact.rs index 69de74ae..acfee995 100644 --- a/crates/ev-cli/src/commands/interact.rs +++ b/crates/ev-cli/src/commands/interact.rs @@ -2,6 +2,8 @@ use crate::theme::CliTheme; use dialoguer::{Input, Select}; use indicatif::{ProgressBar, ProgressStyle}; +use self::validators::ValidationError; + pub mod validators { use lazy_static; use regex::Regex; @@ -61,7 +63,7 @@ pub mod validators { } } - pub fn validate_function_name(name: &str) -> Result<(), ValidationError> { + pub fn validate_function_name(name: &String) -> Result<(), ValidationError> { lazy_static::lazy_static!( static ref NAME_REGEX: Regex = Regex::new(r"^[A-Za-z0-9]([-_]?[A-Za-z0-9])*$").unwrap(); ); @@ -73,7 +75,7 @@ pub mod validators { Ok(()) } - pub fn validate_function_language(language: &str) -> Result<(), ValidationError> { + pub fn validate_function_language(language: &String) -> Result<(), ValidationError> { lazy_static::lazy_static!( static ref LANGUAGE_REGEX: Regex = Regex::new(r"\b(?:node|python)@\d+(\.\d+)?\b").unwrap(); ); @@ -110,7 +112,7 @@ pub fn validated_input( prompt: T, allow_empty: bool, validator: Box, -) -> Option +) -> Result where T: std::fmt::Display, { @@ -122,18 +124,33 @@ where .allow_empty(allow_empty) .validate_with(validator) .interact() - .ok() } -pub fn select(options: &Vec, default: usize, prompt: Option) -> Option { +pub fn select(options: &Vec, default: usize, prompt: T) -> Option +where + T: std::fmt::Display, +{ let theme = CliTheme::default(); let mut select_obj = Select::with_theme(&theme); - if let Some(prompt) = prompt { - select_obj.with_prompt(prompt); - } + select_obj.with_prompt(prompt.to_string()); select_obj.items(options).default(default).interact().ok() } +pub fn preset_input(prompt: S, preset: T) -> Option +where + S: std::fmt::Display, + T: std::fmt::Display, +{ + let theme = CliTheme::default(); + let mut input: Input = Input::with_theme(&theme); + + input + .with_prompt(prompt.to_string()) + .default(preset.to_string()) + .interact() + .ok() +} + /// To make quiet mode integration more simple /// OptionalProgressBar can be used - all functions called /// as normal, but will result in No-Ops during quiet mode diff --git a/crates/ev-cli/src/commands/relay/create.rs b/crates/ev-cli/src/commands/relay/create.rs index a51ac773..942a2985 100644 --- a/crates/ev-cli/src/commands/relay/create.rs +++ b/crates/ev-cli/src/commands/relay/create.rs @@ -29,33 +29,33 @@ pub enum CreateError { "A Relay configuration file already exists at the path: {0}, use the --force parameter to overwrite the existing file" )] FileAlreadyExists(String), - #[error("An error occured while writing the Relay configuration file: {0}]")] - WriteError(#[from] std::io::Error), + #[error("An IO error occurred: {0}")] + Io(#[from] std::io::Error), #[error( "A domain must be chosen to create a Relay. Use the --domain flag to provide one ahead of time." )] NoDomain, #[error("An error occurred while creating the relay: {0}")] - ApiError(#[from] ApiError), + Api(#[from] ApiError), #[error("An error occured while parsing the relay configuration: {0}")] - ParseError(#[from] serde_json::Error), + Parse(#[from] serde_json::Error), } impl CmdOutput for CreateError { fn code(&self) -> String { match self { CreateError::FileAlreadyExists(_) => "relay-file-already-exists", - CreateError::WriteError(_) => "relay-write-error", + CreateError::Io(_) => "relay-write-error", CreateError::NoDomain => "relay-no-domain", - CreateError::ApiError(_) => "relay-api-error", - CreateError::ParseError(_) => "relay-parse-error", + CreateError::Api(_) => "relay-api-error", + CreateError::Parse(_) => "relay-parse-error", } .to_string() } fn exitcode(&self) -> crate::errors::ExitCode { match self { - CreateError::WriteError(_) => crate::errors::CANTCREAT, + CreateError::Io(_) => crate::errors::IOERR, _ => crate::errors::GENERAL, } } @@ -100,8 +100,7 @@ pub async fn run(args: CreateArgs, auth: BasicAuth) -> Result String { + "index.handler".to_string() +} + +impl TryFrom<&std::path::PathBuf> for FunctionToml { + type Error = FunctionTomlError; + + fn try_from(path: &std::path::PathBuf) -> Result { + if !path.try_exists()? { + return Err(FunctionTomlError::ConfigNotFound( + path.to_string_lossy().into(), + )); + } + + let file = File::open(&path)?; + let mut buf_reader = BufReader::new(file); + let mut contents = String::new(); + buf_reader.read_to_string(&mut contents)?; + + Ok(toml::from_str(&contents)?) + } +} diff --git a/crates/ev-cli/src/main.rs b/crates/ev-cli/src/main.rs index 8e5cf734..db991838 100644 --- a/crates/ev-cli/src/main.rs +++ b/crates/ev-cli/src/main.rs @@ -11,6 +11,7 @@ mod auth; mod commands; mod errors; mod fs; +mod function; mod relay; mod theme; mod tty;