Skip to content

Commit

Permalink
feat: better and descriptive errors with anyhow
Browse files Browse the repository at this point in the history
  • Loading branch information
SyedAhkam committed Aug 12, 2023
1 parent ec2734d commit 6f295ce
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 47 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ which = "4.4.0"
itertools = "0.11.0"
ron = "0.8.0"
serde = { version = "1.0.183", features = ["derive"] }
anyhow = "1.0.72"
8 changes: 6 additions & 2 deletions src/commands/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use anyhow::{Context, Result};
use clap::{ArgAction, Parser};

#[derive(Parser, Debug)]
Expand All @@ -7,18 +8,21 @@ pub struct Build {
release: bool,
}

pub fn handle(args: Build) {
pub fn handle(args: Build) -> Result<()> {
// Decide gradle subcommand to use
let cmd = match args.release {
true => "assembleRelease",
false => "assembleDebug",
};

// Invoke gradle as child process
let status = android_cli::invoke_gradle_command(cmd).unwrap();
let status =
android_cli::invoke_gradle_command(cmd).context("failed to invoke gradle command")?;

match status.success() {
true => println!("Success!"),
false => println!("Failed while executing Gradle."),
}

Ok(())
}
59 changes: 34 additions & 25 deletions src/commands/create.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::{collections::BTreeMap, path::PathBuf};

use anyhow::{Context, Result, anyhow};
use clap::Parser;

use std::{collections::BTreeMap, path::PathBuf};

use crate::utils::prompt_for_input;

const DEFAULT_COMPILE_SDK_VERSION: &str = "33";
Expand Down Expand Up @@ -42,17 +43,17 @@ pub struct Create {
min_sdk_version: u32,
}

fn get_vars(args: &Create) -> BTreeMap<String, String> {
fn get_vars(args: &Create) -> Result<BTreeMap<String, String>> {
let mut map = BTreeMap::<String, String>::new();

// FIXME: this is a hack, we should use a proper parser
let get_package_id = |package_id: String| {
let mut parts = package_id.split('.');
let domain = parts.next().unwrap();
let org = parts.next().unwrap();
let name = parts.next().unwrap();
let domain = parts.next().ok_or_else(|| anyhow!("domain part missing in package"))?;
let org = parts.next().ok_or_else(|| anyhow!("org part missing in package"))?;
let name = parts.next().ok_or_else(|| anyhow!("name part missing in package"))?;

(domain.to_owned(), org.to_owned(), name.to_owned())
anyhow::Ok((domain.to_owned(), org.to_owned(), name.to_owned()))
};

// Metadata
Expand All @@ -63,7 +64,8 @@ fn get_vars(args: &Create) -> BTreeMap<String, String> {
map.insert("app_name".into(), args.name.as_ref().unwrap().to_owned());

// Package identifiers
let (domain, org, name) = get_package_id(args.package_id.as_ref().unwrap().to_owned());
let (domain, org, name) = get_package_id(args.package_id.as_ref().unwrap().to_owned())
.context("failed to parse package id")?;
map.insert("package_id_domain".into(), domain);
map.insert("package_id_org".into(), org);
map.insert("package_id_name".into(), name);
Expand All @@ -79,63 +81,70 @@ fn get_vars(args: &Create) -> BTreeMap<String, String> {
);
map.insert("min_sdk_version".into(), args.min_sdk_version.to_string());

map
Ok(map)
}

fn post_create(args: Create) {
fn post_create(args: Create) -> Result<()> {
let dest = args.dest.clone().unwrap();

android_cli::create_local_properties_file(&dest, &args.sdk_path.unwrap());
android_cli::create_dot_file(&dest, args.package_id.unwrap());
android_cli::create_local_properties_file(&dest, &args.sdk_path.unwrap())?;
android_cli::create_dot_file(&dest, args.package_id.unwrap())?;

Ok(())
}

fn ensure_valid_args(args: Create) -> Create {
fn ensure_valid_args(args: Create) -> Result<Create> {
let dest = args
.dest
.unwrap_or_else(|| prompt_for_input("Enter destination path").into());
.unwrap_or_else(|| prompt_for_input("Enter destination path").unwrap().into());

let dest_folder_name = dest.file_name().unwrap().to_str().unwrap().to_owned(); // FIXME: this could fail on non-unicode paths

let sdk_path = args
.sdk_path
.unwrap_or_else(|| std::env::var("ANDROID_SDK_ROOT").expect("ANDROID_SDK_ROOT not set"));
let sdk_path = args.sdk_path.unwrap_or_else(|| {
std::env::var("ANDROID_SDK_ROOT")
.context("ANDROID_SDK_ROOT not set")
.unwrap()
});

let project_name = args
.project_name
.unwrap_or_else(|| dest_folder_name)
.unwrap_or(dest_folder_name) // defaults to dest folder name
.to_lowercase()
.replace(" ", "_"); // Perform some cleanup

let app_name = args.name.unwrap_or_else(|| project_name.clone());

let package_id = args.package_id.unwrap_or_else(|| {
prompt_for_input("Enter package identifier [example: com.example.demo]")
.unwrap()
.to_lowercase()
.replace(" ", "_")
});

Create {
Ok(Create {
dest: Some(dest),
sdk_path: Some(sdk_path),
project_name: Some(project_name),
name: Some(app_name),
package_id: Some(package_id),
..args
}
})
}

pub fn handle(args: Create) {
let args = ensure_valid_args(args);
pub fn handle(args: Create) -> Result<()> {
let args = ensure_valid_args(args)?;

// Prepare variables to substitute
let vars = get_vars(&args);
let vars = get_vars(&args)?;

// Copy template
let dest = args.dest.as_ref().unwrap();
android_cli::copy_template(dest, vars);
android_cli::copy_template(dest, vars)?;

// Perform post init tasks
post_create(args);
post_create(args)?;

println!("Project created successfully");

Ok(())
}
11 changes: 7 additions & 4 deletions src/commands/install.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::PathBuf;

use clap::{ArgAction, Parser};
use anyhow::{Result, Context};

use std::path::PathBuf;

#[derive(Parser, Debug)]
pub struct Install {
Expand All @@ -9,7 +10,7 @@ pub struct Install {
release: bool,
}

pub fn handle(args: Install) {
pub fn handle(args: Install) -> Result<()> {
let output_dir = PathBuf::from("app/build/outputs/apk");

let apk_path = match args.release {
Expand All @@ -21,10 +22,12 @@ pub fn handle(args: Install) {
"install",
apk_path.to_str().unwrap()
])
.unwrap();
.context("failed to run adb command")?;

match status.success() {
true => println!("Successfully installed APK."),
false => eprintln!("Failed to install APK"),
};

Ok(())
}
31 changes: 19 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod utils;
use guidon::{GitOptions, Guidon, TryNew};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use anyhow::{Result, Context};

use std::{
collections::BTreeMap,
Expand All @@ -25,28 +26,32 @@ struct DotAndroid {
pub gen_at_version: String,
}

pub fn copy_template(dest: &Path, vars: BTreeMap<String, String>) {
pub fn copy_template(dest: &Path, vars: BTreeMap<String, String>) -> Result<()> {
let git_options = GitOptions::builder()
.repo(DEFAULT_TEMPLATE_REPO)
.rev(TEMPLATE_REV)
.build()
.unwrap();

let mut guidon = Guidon::try_new(git_options).unwrap();
let mut guidon = Guidon::try_new(git_options)?;

guidon.variables(vars);
guidon.apply_template(dest).unwrap();
guidon.apply_template(dest)?;

Ok(())
}

pub fn create_local_properties_file(root: &Path, sdk_path: &str) {
pub fn create_local_properties_file(root: &Path, sdk_path: &str) -> Result<()> {
let prop_file_path = PathBuf::new().join(root).join("local.properties");

let content = format!("sdk.dir={}", sdk_path);
std::fs::write(prop_file_path, content).expect("Unable to write local.properties file")
std::fs::write(prop_file_path, content).context("Unable to write local.properties file")?;

Ok(())
}

pub fn invoke_gradle_command(cmd: &str) -> Result<ExitStatus, Box<dyn std::error::Error>> {
let gradle_path = find_gradle().expect("ERROR: Gradle not found on system");
pub fn invoke_gradle_command(cmd: &str) -> Result<ExitStatus> {
let gradle_path = find_gradle().context("ERROR: Gradle not found on system")?;

let mut run = Command::new(gradle_path);
run.arg(cmd);
Expand All @@ -60,8 +65,8 @@ pub fn invoke_gradle_command(cmd: &str) -> Result<ExitStatus, Box<dyn std::error
Ok(run.status()?)
}

pub fn invoke_adb_command(args: &[&str]) -> Result<ExitStatus, Box<dyn std::error::Error>> {
let adb_path = find_adb().expect("ERROR: ADB not found on system");
pub fn invoke_adb_command(args: &[&str]) -> Result<ExitStatus> {
let adb_path = find_adb().context("ERROR: ADB not found on system")?;

let mut run = Command::new(adb_path);
run.args(args);
Expand All @@ -75,7 +80,7 @@ pub fn invoke_adb_command(args: &[&str]) -> Result<ExitStatus, Box<dyn std::erro
Ok(run.status()?)
}

pub fn create_dot_file(dest: &Path, package_id: String) {
pub fn create_dot_file(dest: &Path, package_id: String) -> Result<()> {
// Construct the structure
let dot_android = DotAndroid {
package_id,
Expand All @@ -84,12 +89,14 @@ pub fn create_dot_file(dest: &Path, package_id: String) {

// Serialize into Ron
let mut ron_contents =
ron::ser::to_string_pretty(&dot_android, ron::ser::PrettyConfig::default()).unwrap();
ron::ser::to_string_pretty(&dot_android, ron::ser::PrettyConfig::default())?;

// Add a comment at top
ron_contents.insert_str(0, DOTFILE_COMMENT);

// Write to file
let path = PathBuf::from(dest).join(".android");
std::fs::write(path, ron_contents).expect("failed to write .android file");
std::fs::write(path, ron_contents).context("failed to write .android file")?;

Ok(())
}
6 changes: 5 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ fn main() {

let args = Cli::parse();

match args.command {
let result = match args.command {
SubCommand::Create(args) => commands::create::handle(args),
SubCommand::Build(args) => commands::build::handle(args),
SubCommand::Install(args) => commands::install::handle(args)
};

if result.is_err() {
eprintln!("ERROR: {:?}", result.unwrap_err());
}
}
7 changes: 4 additions & 3 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
#![allow(dead_code)]

use anyhow::{Result, Context};
use which::which;

pub fn prompt_for_input(prompt: &str) -> String {
dialoguer::Input::<String>::with_theme(&dialoguer::theme::ColorfulTheme::default())
pub fn prompt_for_input(prompt: &str) -> Result<String> {
Ok(dialoguer::Input::<String>::with_theme(&dialoguer::theme::ColorfulTheme::default())
.with_prompt(prompt)
.interact_text()
.unwrap()
.context("failed to prompt user")?)
}

pub fn find_gradle() -> Option<String> {
Expand Down

0 comments on commit 6f295ce

Please sign in to comment.