Skip to content

Commit

Permalink
feat: shuttle init --from (#984)
Browse files Browse the repository at this point in the history
* Refactor init args

* fix test + details

* Polish run integration test

* Add test-case. Try to prevent errors, still not error-free.

* Prompt for other templates

* better help

* Use more of the cargo-generate features, rename args

* bump lockfile

* fix git init

* comments

* Hint serenity users about idle

* remove prompt for list of all templates, improve hints

* not 1.70 too soon

* feedback cleanup

* fix tests

* Better hint

* fix: remove init project

---------

Co-authored-by: oddgrd <29732646+oddgrd@users.noreply.github.com>
  • Loading branch information
jonaro00 and oddgrd authored Jun 20, 2023
1 parent 375b616 commit 73cf246
Show file tree
Hide file tree
Showing 10 changed files with 808 additions and 576 deletions.
582 changes: 307 additions & 275 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 @@ -27,7 +25,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 @@ -231,81 +229,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 @@ -332,27 +361,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

0 comments on commit 73cf246

Please sign in to comment.