Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: shuttle init --from #984

Merged
merged 19 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
641 changes: 307 additions & 334 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ bytes = "1.3.0"
cap-std = "1.0.2"
cargo_metadata = "0.15.3"
chrono = { version = "0.4.23", default-features = false }
clap = { version = "4.0.27", features = ["derive"] }
clap = { version = "4.2.7", features = ["derive"] }
crossbeam-channel = "0.5.7"
crossterm = "0.26.0"
ctor = "0.1.26"
Expand Down
2 changes: 1 addition & 1 deletion cargo-shuttle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ bollard = { workspace = true }
cargo_metadata = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["env"] }
clap_complete = "4.1.5"
clap_complete = "4.3.1"
crossbeam-channel = { workspace = true }
crossterm = { workspace = true }
dialoguer = { version = "0.10.4", features = ["fuzzy-select"] }
Expand Down
186 changes: 136 additions & 50 deletions cargo-shuttle/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{
path::PathBuf,
};

use anyhow::Context;
use anyhow::{bail, Context};
use cargo_metadata::MetadataCommand;
use clap::{
builder::{OsStringValueParser, PossibleValue, TypedValueParser},
Expand All @@ -15,8 +15,6 @@ use clap_complete::Shell;
use shuttle_common::{models::project::IDLE_MINUTES, project::ProjectName};
use uuid::Uuid;

use crate::init::Template;

#[derive(Parser)]
#[command(
version,
Expand All @@ -28,7 +26,7 @@ use crate::init::Template;
.required(false)
.hide(true))
)]
pub struct Args {
pub struct ShuttleArgs {
#[command(flatten)]
pub project_args: ProjectArgs,
/// Run this command against the API at the supplied URL
Expand Down Expand Up @@ -228,81 +226,112 @@ pub struct RunArgs {
/// Use 0.0.0.0 instead of localhost (for usage with local external devices)
#[arg(long)]
pub external: bool,
/// Use release mode for building the project.
/// Use release mode for building the project
#[arg(long, short = 'r')]
pub release: bool,
}

#[derive(Parser, Debug)]
#[derive(Parser, Clone, Debug)]
pub struct InitArgs {
/// Initialize the project with a template
#[arg(long, short, value_enum)]
/// Clone a starter template from shuttle's official examples
#[arg(long, short, value_enum, conflicts_with_all = &["from", "subfolder"])]
pub template: Option<InitTemplateArg>,
/// Clone a template from a git repository or local path using cargo-generate
#[arg(long)]
pub from: Option<String>,
/// Path to the template in the source (used with --from)
#[arg(long, requires = "from")]
pub subfolder: Option<String>,

/// Path where to place the new shuttle project
#[arg(default_value = ".", value_parser = OsStringValueParser::new().try_map(parse_init_path))]
pub path: PathBuf,

/// Whether to create the environment for this project on shuttle
#[arg(long)]
pub create_env: bool,
#[command(flatten)]
pub login_args: LoginArgs,
/// Path to initialize a new shuttle project
#[arg(default_value = ".", value_parser = OsStringValueParser::new().try_map(parse_init_path))]
pub path: PathBuf,
}

#[derive(ValueEnum, Clone, Debug)]
#[derive(ValueEnum, Clone, Debug, strum::Display, strum::EnumIter)]
#[strum(serialize_all = "kebab-case")]
pub enum InitTemplateArg {
/// Initialize with actix-web framework
/// Actix-web framework
ActixWeb,
/// Initialize with axum framework
/// Axum web framework
Axum,
/// Initialize with poem framework
/// Poem web framework
Poem,
/// Initialize with poise framework
/// Poise Discord framework
Poise,
/// Initialize with rocket framework
/// Rocket web framework
Rocket,
/// Initialize with salvo framework
/// Salvo web framework
Salvo,
/// Initialize with serenity framework
/// Serenity Discord framework
Serenity,
/// Initialize with tide framework
Tide,
/// Initialize with thruster framework
/// Thruster web framework
Thruster,
/// Initialize with tower framework
/// Tide web framework
Tide,
/// Tower web framework
Tower,
/// Initialize with warp framework
/// Warp web framework
Warp,
/// Initialize with no template
/// No template - Custom empty service
None,
}

pub const EXAMPLES_REPO: &str = "https://github.com/shuttle-hq/shuttle-examples";

#[derive(Clone, Debug, PartialEq)]
pub struct TemplateLocation {
pub auto_path: String,
pub subfolder: Option<String>,
}

impl InitArgs {
/// `None` -> No template chosen, ask for it
///
/// `Some(Template::None)` -> Init with a blank cargo project
pub fn framework(&self) -> Option<Template> {
// Why separate enums?
// Future might have more options that pre-defined templates
self.template.as_ref().map(|t| {
use InitTemplateArg::*;
match t {
ActixWeb => Template::ActixWeb,
Axum => Template::Axum,
Poem => Template::Poem,
Poise => Template::Poise,
Rocket => Template::Rocket,
Salvo => Template::Salvo,
Serenity => Template::Serenity,
Tide => Template::Tide,
Thruster => Template::Thruster,
Tower => Template::Tower,
Warp => Template::Warp,
None => Template::None,
}
pub fn git_template(&self) -> anyhow::Result<Option<TemplateLocation>> {
if self.from.is_some() && self.template.is_some() {
bail!("Template and From args can not be set at the same time.");
}
Ok(if let Some(from) = self.from.clone() {
Some(TemplateLocation {
auto_path: from,
subfolder: self.subfolder.clone(),
})
} else {
self.template.as_ref().map(|t| t.template())
})
}
}

impl InitTemplateArg {
pub fn template(&self) -> TemplateLocation {
use InitTemplateArg::*;
let path = match self {
ActixWeb => "actix-web/hello-world",
Axum => "axum/hello-world",
Poem => "poem/hello-world",
Poise => "poise/hello-world",
Rocket => "rocket/hello-world",
Salvo => "salvo/hello-world",
Serenity => "serenity/hello-world",
Thruster => "thruster/hello-world",
Tide => "tide/hello-world",
Tower => "tower/hello-world",
Warp => "warp/hello-world",
None => "custom/none",
};

TemplateLocation {
auto_path: EXAMPLES_REPO.into(),
subfolder: Some(path.to_string()),
}
}
}

/// Helper function to parse and return the absolute path
fn parse_path(path: OsString) -> Result<PathBuf, String> {
dunce::canonicalize(&path).map_err(|e| format!("could not turn {path:?} into a real path: {e}"))
Expand All @@ -329,27 +358,84 @@ mod tests {

#[test]
fn test_init_args_framework() {
// pre-defined template (only hello world)
let init_args = InitArgs {
template: Some(InitTemplateArg::Tower),
from: None,
subfolder: None,
create_env: false,
login_args: LoginArgs { api_key: None },
path: PathBuf::new(),
};
assert_eq!(
init_args.git_template().unwrap(),
Some(TemplateLocation {
auto_path: EXAMPLES_REPO.into(),
subfolder: Some("tower/hello-world".into())
})
);

// pre-defined template (multiple)
let init_args = InitArgs {
template: Some(InitTemplateArg::Axum),
from: None,
subfolder: None,
create_env: false,
login_args: LoginArgs { api_key: None },
path: PathBuf::new(),
};
assert_eq!(init_args.framework(), Some(Template::Axum));
assert_eq!(
init_args.git_template().unwrap(),
Some(TemplateLocation {
auto_path: EXAMPLES_REPO.into(),
subfolder: Some("axum/hello-world".into())
})
);

// pre-defined "none" template
let init_args = InitArgs {
template: Some(InitTemplateArg::None),
from: None,
subfolder: None,
create_env: false,
login_args: LoginArgs { api_key: None },
path: PathBuf::new(),
};
assert_eq!(
init_args.git_template().unwrap(),
Some(TemplateLocation {
auto_path: EXAMPLES_REPO.into(),
subfolder: Some("custom/none".into())
})
);

// git template with path
let init_args = InitArgs {
template: None,
from: Some("https://github.com/some/repo".into()),
subfolder: Some("some/path".into()),
create_env: false,
login_args: LoginArgs { api_key: None },
path: PathBuf::new(),
};
assert_eq!(init_args.framework(), Some(Template::None));
assert_eq!(
init_args.git_template().unwrap(),
Some(TemplateLocation {
auto_path: "https://github.com/some/repo".into(),
subfolder: Some("some/path".into())
})
);

// No template or repo chosen
let init_args = InitArgs {
template: None,
from: None,
subfolder: None,
create_env: false,
login_args: LoginArgs { api_key: None },
path: PathBuf::new(),
};
assert_eq!(init_args.framework(), None);
assert_eq!(init_args.git_template().unwrap(), None);
}

#[test]
Expand Down
Loading