Switch everything to StructOpt (#277)

* Add types for Rojo's subcommands

* Flesh out CLI types

* Port everything to structopt instead of clap
This commit is contained in:
Lucien Greathouse
2019-12-12 14:30:47 -08:00
committed by GitHub
parent 8b1e85fbb4
commit 47c7f63d75
9 changed files with 296 additions and 235 deletions

View File

@@ -1,63 +1,19 @@
use std::{
env, panic,
path::{Path, PathBuf},
process,
use std::{panic, process};
use failure::Error;
use log::error;
use structopt::StructOpt;
use librojo::{
cli::{Options, Subcommand},
commands,
};
use clap::{clap_app, ArgMatches};
use log::error;
use librojo::commands;
fn make_path_absolute(value: &Path) -> PathBuf {
if value.is_absolute() {
PathBuf::from(value)
} else {
let current_dir = env::current_dir().unwrap();
current_dir.join(value)
}
}
fn main() {
let app = clap_app!(Rojo =>
(version: env!("CARGO_PKG_VERSION"))
(author: env!("CARGO_PKG_AUTHORS"))
(about: env!("CARGO_PKG_DESCRIPTION"))
(@arg verbose: --verbose -v +multiple +global "Sets verbosity level. Can be specified multiple times.")
(@subcommand init =>
(about: "Creates a new Rojo project.")
(@arg PATH: "Path to the place to create the project. Defaults to the current directory.")
(@arg kind: --kind +takes_value "The kind of project to create, 'place' or 'model'. Defaults to place.")
)
(@subcommand serve =>
(about: "Serves the project's files for use with the Rojo Studio plugin.")
(@arg PROJECT: "Path to the project to serve. Defaults to the current directory.")
(@arg port: --port +takes_value "The port to listen on. Defaults to 34872.")
)
(@subcommand build =>
(about: "Generates a model or place file from the project.")
(@arg PROJECT: "Path to the project to serve. Defaults to the current directory.")
(@arg output: --output -o +takes_value +required "Where to output the result.")
)
(@subcommand upload =>
(about: "Generates a place or model file out of the project and uploads it to Roblox.")
(@arg PROJECT: "Path to the project to upload. Defaults to the current directory.")
(@arg kind: --kind +takes_value "The kind of asset to generate, 'place', or 'model'. Defaults to place.")
(@arg cookie: --cookie +takes_value "Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.")
(@arg asset_id: --asset_id +takes_value +required "Asset ID to upload to.")
)
);
let matches = app.get_matches();
let options = Options::from_args();
{
let verbosity = matches.occurrences_of("verbose");
let log_filter = match verbosity {
let log_filter = match options.verbosity {
0 => "warn",
1 => "warn,librojo=info",
2 => "warn,librojo=trace",
@@ -71,15 +27,14 @@ fn main() {
.init();
}
let result = panic::catch_unwind(|| match matches.subcommand() {
("init", Some(sub_matches)) => start_init(sub_matches),
("serve", Some(sub_matches)) => start_serve(sub_matches),
("build", Some(sub_matches)) => start_build(sub_matches),
("upload", Some(sub_matches)) => start_upload(sub_matches),
_ => eprintln!("Usage: rojo <SUBCOMMAND>\nUse 'rojo help' for more help."),
let panic_result = panic::catch_unwind(|| {
if let Err(err) = run(options.subcommand) {
log::error!("{}", err);
process::exit(1);
}
});
if let Err(error) = result {
if let Err(error) = panic_result {
let message = match error.downcast_ref::<&str>() {
Some(message) => message.to_string(),
None => match error.downcast_ref::<String>() {
@@ -93,6 +48,17 @@ fn main() {
}
}
fn run(subcommand: Subcommand) -> Result<(), Error> {
match subcommand {
Subcommand::Init(init_options) => commands::init(init_options)?,
Subcommand::Serve(serve_options) => commands::serve(serve_options)?,
Subcommand::Build(build_options) => commands::build(build_options)?,
Subcommand::Upload(upload_options) => commands::upload(upload_options)?,
}
Ok(())
}
fn show_crash_message(message: &str) {
error!("Rojo crashed!");
error!("This is a bug in Rojo.");
@@ -101,113 +67,3 @@ fn show_crash_message(message: &str) {
error!("");
error!("Details: {}", message);
}
fn start_init(sub_matches: &ArgMatches) {
let fuzzy_project_path =
make_path_absolute(Path::new(sub_matches.value_of("PATH").unwrap_or("")));
let kind = sub_matches.value_of("kind");
let options = commands::InitOptions {
fuzzy_project_path,
kind,
};
match commands::init(&options) {
Ok(_) => {}
Err(e) => {
error!("{}", e);
process::exit(1);
}
}
}
fn start_serve(sub_matches: &ArgMatches) {
let fuzzy_project_path = match sub_matches.value_of("PROJECT") {
Some(v) => make_path_absolute(Path::new(v)),
None => std::env::current_dir().unwrap(),
};
let port = match sub_matches.value_of("port") {
Some(v) => match v.parse::<u16>() {
Ok(port) => Some(port),
Err(_) => {
error!("Invalid port {}", v);
process::exit(1);
}
},
None => None,
};
let options = commands::ServeOptions {
fuzzy_project_path,
port,
};
match commands::serve(&options) {
Ok(_) => {}
Err(e) => {
error!("{}", e);
process::exit(1);
}
}
}
fn start_build(sub_matches: &ArgMatches) {
let fuzzy_project_path = match sub_matches.value_of("PROJECT") {
Some(v) => make_path_absolute(Path::new(v)),
None => std::env::current_dir().unwrap(),
};
let output_file = make_path_absolute(Path::new(sub_matches.value_of("output").unwrap()));
let options = commands::BuildOptions {
fuzzy_project_path,
output_file,
output_kind: None, // TODO: Accept from argument
};
match commands::build(&options) {
Ok(_) => {}
Err(e) => {
error!("{}", e);
process::exit(1);
}
}
}
fn start_upload(sub_matches: &ArgMatches) {
let fuzzy_project_path = match sub_matches.value_of("PROJECT") {
Some(v) => make_path_absolute(Path::new(v)),
None => std::env::current_dir().unwrap(),
};
let kind = sub_matches.value_of("kind");
let auth_cookie = sub_matches.value_of("cookie").map(Into::into);
let asset_id: u64 = {
let arg = sub_matches.value_of("asset_id").unwrap();
match arg.parse() {
Ok(v) => v,
Err(_) => {
error!("Invalid place ID {}", arg);
process::exit(1);
}
}
};
let options = commands::UploadOptions {
fuzzy_project_path,
auth_cookie,
asset_id,
kind,
};
match commands::upload(options) {
Ok(_) => {}
Err(e) => {
error!("{}", e);
process::exit(1);
}
}
}

191
src/cli.rs Normal file
View File

@@ -0,0 +1,191 @@
//! Defines Rojo's CLI through structopt types.
#![deny(missing_docs)]
use std::{env, error::Error, fmt, path::PathBuf, str::FromStr};
use structopt::StructOpt;
/// Trick used with structopt to get the initial working directory of the
/// process and store it for use in default values.
fn working_dir() -> &'static str {
lazy_static::lazy_static! {
static ref INITIAL_WORKING_DIR: String = {
env::current_dir().unwrap().display().to_string()
};
}
&INITIAL_WORKING_DIR
}
/// Command line options that Rojo accepts, defined using the structopt crate.
#[derive(Debug, StructOpt)]
#[structopt(name = "Rojo", about, author)]
pub struct Options {
/// Sets verbosity level. Can be specified multiple times.
#[structopt(long = "verbose", short, parse(from_occurrences))]
pub verbosity: u8,
/// Subcommand to run in this invocation.
#[structopt(subcommand)]
pub subcommand: Subcommand,
}
/// All of Rojo's subcommands.
#[derive(Debug, StructOpt)]
pub enum Subcommand {
/// Creates a new Rojo project.
Init(InitCommand),
/// Serves the project's files for use with the Rojo Studio plugin.
Serve(ServeCommand),
/// Generates a model or place file from the project.
Build(BuildCommand),
/// Generates a place or model file out of the project and uploads it to Roblox.
Upload(UploadCommand),
}
/// Initializes a new Rojo project.
#[derive(Debug, StructOpt)]
pub struct InitCommand {
/// Path to the place to create the project. Defaults to the current directory.
#[structopt(default_value = &working_dir())]
pub path: PathBuf,
/// The kind of project to create, 'place' or 'model'. Defaults to place.
#[structopt(long, default_value = "place")]
pub kind: InitKind,
}
/// The templates we support for initializing a Rojo project.
#[derive(Debug, Clone, Copy)]
pub enum InitKind {
/// A place that matches what File -> New does in Roblox Studio.
Place,
/// An empty model, suitable for a library or plugin.
Model,
}
impl FromStr for InitKind {
type Err = InitKindParseError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"place" => Ok(InitKind::Place),
"model" => Ok(InitKind::Model),
_ => Err(InitKindParseError {
attempted: source.to_owned(),
}),
}
}
}
/// Error type for failing to parse an `InitKind`.
#[derive(Debug)]
pub struct InitKindParseError {
attempted: String,
}
impl Error for InitKindParseError {}
impl fmt::Display for InitKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"Invalid init kind '{}'. Valid kinds are: place, model",
self.attempted
)
}
}
/// Expose a Rojo project through a web server that can communicate with the
/// Rojo Roblox Studio plugin, or be visited by the user in the browser.
#[derive(Debug, StructOpt)]
pub struct ServeCommand {
/// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = &working_dir())]
pub project: PathBuf,
/// The port to listen on. Defaults to the project's preference, or 34872 if
/// it has none.
#[structopt(long)]
pub port: Option<u16>,
}
/// Build a Rojo project into a file.
#[derive(Debug, StructOpt)]
pub struct BuildCommand {
/// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = &working_dir())]
pub project: PathBuf,
/// Where to output the result.
#[structopt(long, short)]
pub output: PathBuf,
}
/// Build and upload a Rojo project to Roblox.com.
#[derive(Debug, StructOpt)]
pub struct UploadCommand {
/// Path to the project to upload. Defaults to the current directory.
#[structopt(default_value = &working_dir())]
pub project: PathBuf,
/// The kind of asset to generate, 'place', or 'model'. Defaults to place.
#[structopt(long, default_value = "place")]
pub kind: UploadKind,
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
#[structopt(long)]
pub cookie: Option<String>,
/// Asset ID to upload to.
#[structopt(long = "asset_id")]
pub asset_id: u64,
}
/// The kind of asset to upload to the website. Affects what endpoints Rojo uses
/// and changes how the asset is built.
#[derive(Debug, Clone, Copy)]
pub enum UploadKind {
/// Upload to a place.
Place,
/// Upload to a model-like asset, like a Model, Plugin, or Package.
Model,
}
impl FromStr for UploadKind {
type Err = UploadKindParseError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"place" => Ok(UploadKind::Place),
"model" => Ok(UploadKind::Model),
_ => Err(UploadKindParseError {
attempted: source.to_owned(),
}),
}
}
}
/// Error type for failing to parse an `UploadKind`.
#[derive(Debug)]
pub struct UploadKindParseError {
attempted: String,
}
impl Error for UploadKindParseError {}
impl fmt::Display for UploadKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"Invalid upload kind '{}'. Valid kinds are: place, model",
self.attempted
)
}
}

View File

@@ -1,27 +1,27 @@
use std::{
fs::File,
io::{self, BufWriter, Write},
path::PathBuf,
};
use failure::Fail;
use crate::{
cli::BuildCommand,
common_setup,
project::ProjectLoadError,
vfs::{FsError, RealFetcher, Vfs, WatchMode},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputKind {
enum OutputKind {
Rbxmx,
Rbxlx,
Rbxm,
Rbxl,
}
fn detect_output_kind(options: &BuildOptions) -> Option<OutputKind> {
let extension = options.output_file.extension()?.to_str()?;
fn detect_output_kind(options: &BuildCommand) -> Option<OutputKind> {
let extension = options.output.extension()?.to_str()?;
match extension {
"rbxlx" => Some(OutputKind::Rbxlx),
@@ -32,13 +32,6 @@ fn detect_output_kind(options: &BuildOptions) -> Option<OutputKind> {
}
}
#[derive(Debug)]
pub struct BuildOptions {
pub fuzzy_project_path: PathBuf,
pub output_file: PathBuf,
pub output_kind: Option<OutputKind>,
}
#[derive(Debug, Fail)]
pub enum BuildError {
#[fail(display = "Could not detect what kind of file to create")]
@@ -72,22 +65,19 @@ fn xml_encode_config() -> rbx_xml::EncodeOptions {
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
}
pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
let output_kind = options
.output_kind
.or_else(|| detect_output_kind(options))
.ok_or(BuildError::UnknownOutputKind)?;
pub fn build(options: BuildCommand) -> Result<(), BuildError> {
let output_kind = detect_output_kind(&options).ok_or(BuildError::UnknownOutputKind)?;
log::debug!("Hoping to generate file of type {:?}", output_kind);
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new(RealFetcher::new(WatchMode::Disabled));
let (_maybe_project, tree) = common_setup::start(&options.fuzzy_project_path, &vfs);
let (_maybe_project, tree) = common_setup::start(&options.project, &vfs);
let root_id = tree.get_root_id();
log::trace!("Opening output file for write");
let mut file = BufWriter::new(File::create(&options.output_file)?);
let mut file = BufWriter::new(File::create(&options.output)?);
match output_kind {
OutputKind::Rbxmx => {

View File

@@ -1,17 +1,12 @@
use std::path::PathBuf;
use failure::Fail;
use crate::project::{Project, ProjectInitError};
use crate::{
cli::{InitCommand, InitKind},
project::{Project, ProjectInitError},
};
#[derive(Debug, Fail)]
pub enum InitError {
#[fail(
display = "Invalid project kind '{}', valid kinds are 'place' and 'model'",
_0
)]
InvalidKind(String),
#[fail(display = "Project init error: {}", _0)]
ProjectInitError(#[fail(cause)] ProjectInitError),
}
@@ -20,23 +15,16 @@ impl_from!(InitError {
ProjectInitError => ProjectInitError,
});
#[derive(Debug)]
pub struct InitOptions<'a> {
pub fuzzy_project_path: PathBuf,
pub kind: Option<&'a str>,
}
pub fn init(options: &InitOptions) -> Result<(), InitError> {
pub fn init(options: InitCommand) -> Result<(), InitError> {
let (project_path, project_kind) = match options.kind {
Some("place") | None => {
let path = Project::init_place(&options.fuzzy_project_path)?;
InitKind::Place => {
let path = Project::init_place(&options.path)?;
(path, "place")
}
Some("model") => {
let path = Project::init_model(&options.fuzzy_project_path)?;
InitKind::Model => {
let path = Project::init_model(&options.path)?;
(path, "model")
}
Some(invalid) => return Err(InitError::InvalidKind(invalid.to_string())),
};
println!(

View File

@@ -1,6 +1,5 @@
use std::{
io::{self, Write},
path::PathBuf,
sync::Arc,
};
@@ -8,6 +7,7 @@ use failure::Fail;
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
use crate::{
cli::ServeCommand,
project::ProjectLoadError,
serve_session::ServeSession,
vfs::{RealFetcher, Vfs, WatchMode},
@@ -16,12 +16,6 @@ use crate::{
const DEFAULT_PORT: u16 = 34872;
#[derive(Debug)]
pub struct ServeOptions {
pub fuzzy_project_path: PathBuf,
pub port: Option<u16>,
}
#[derive(Debug, Fail)]
pub enum ServeError {
#[fail(display = "Couldn't load project: {}", _0)]
@@ -32,10 +26,10 @@ impl_from!(ServeError {
ProjectLoadError => ProjectLoad,
});
pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
pub fn serve(options: ServeCommand) -> Result<(), ServeError> {
let vfs = Vfs::new(RealFetcher::new(WatchMode::Enabled));
let session = Arc::new(ServeSession::new(vfs, &options.fuzzy_project_path));
let session = Arc::new(ServeSession::new(vfs, &options.project));
let port = options
.port

View File

@@ -1,10 +1,9 @@
use std::path::PathBuf;
use failure::Fail;
use reqwest::header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT};
use crate::{
auth_cookie::get_auth_cookie,
cli::UploadCommand,
common_setup,
vfs::{RealFetcher, Vfs, WatchMode},
};
@@ -29,24 +28,16 @@ impl_from!(UploadError {
reqwest::Error => Http,
});
#[derive(Debug)]
pub struct UploadOptions<'a> {
pub fuzzy_project_path: PathBuf,
pub auth_cookie: Option<String>,
pub asset_id: u64,
pub kind: Option<&'a str>,
}
pub fn upload(options: UploadOptions) -> Result<(), UploadError> {
pub fn upload(options: UploadCommand) -> Result<(), UploadError> {
let cookie = options
.auth_cookie
.cookie
.or_else(get_auth_cookie)
.ok_or(UploadError::NeedAuthCookie)?;
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new(RealFetcher::new(WatchMode::Disabled));
let (_maybe_project, tree) = common_setup::start(&options.fuzzy_project_path, &vfs);
let (_maybe_project, tree) = common_setup::start(&options.project, &vfs);
let root_id = tree.get_root_id();
let mut buffer = Vec::new();

View File

@@ -5,6 +5,7 @@
#[macro_use]
mod impl_from;
pub mod cli;
pub mod commands;
// This module is only public for testing right now, and won't be