deft-cli 0.1.3

Deft command line tool
use std::{fs, io};
use std::fs::{File};
use std::path::{Path, PathBuf};
use clap::{Args, Parser, Subcommand};
use inquire::Text;
use inquire::validator::{ErrorMessage, Validation};
use regex::Regex;
use rust_embed::{Embed, RustEmbed};
use serde_json::{Map, Value};

const DEFT_CONFIG_FILE: &str = "deft.config.json";

#[derive(Embed)]
#[folder = "template-android"]
struct AndroidAsset;

#[derive(Embed)]
#[folder = "template-ohos"]
struct OhosAsset;

#[derive(Parser)]
struct CliOptions {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    Init(InitCommand),
}

#[derive(Args)]
pub struct InitCommand {
    #[command(subcommand)]
    platform: InitPlatformCommands,
}

#[derive(Subcommand)]
enum InitPlatformCommands {
    Ohos,
    Android,
}

fn main() {
    let cli_options = CliOptions::parse();
    match cli_options.command {
        Commands::Init(init_command) => {
            match init_command.platform {
                InitPlatformCommands::Ohos => init_ohos(),
                InitPlatformCommands::Android => init_android(),
            }
        }
    }
}

fn is_valid_appid(id: &str) -> bool {
    let reg = Regex::new("^([a-zA-Z_]\\w*)+([.][a-zA-Z_]\\w*)+$").unwrap();
    reg.is_match(id)
}

fn inquire_app_id() -> String {
    Text::new("Input your app id:")
        .with_validator(|str: &str| {
            let v = if is_valid_appid(str) {
                Validation::Valid
            } else {
                Validation::Invalid(ErrorMessage::Default)
            };
            Ok(v)
        })
        .prompt()
        .unwrap()
}

fn replace(path: &PathBuf, search: &str, replacement: &str) {
    let config_content = fs::read_to_string(&path).unwrap().replace(search, replacement);
    fs::write(&path, config_content).unwrap();
}

fn init_ohos() {
    let dst = Path::new("ohos").to_path_buf();
    if dst.exists() {
        println!("{} already exists", dst.display());
        return;
    }
    let app_id = inquire_app_id();
    dist::<OhosAsset>(&dst);

    replace(&dst.join("AppScope/app.json5"), "fun.kason.deftapp", &app_id);
    if let Err(error) =  write_app_id("ohos", &app_id) {
        eprintln!("{}", error);
    }
}


fn write_app_id(platform: &str, app_id: &str) -> Result<(), String> {
    let mut config = load_deft_config()?;
    let root = config.as_object_mut().ok_or("Invalid config found")?;
    let ohos = root.entry(platform)
        .or_insert(Value::Object(Map::new()))
        .as_object_mut().ok_or("Invalid config found")?;
    ohos.insert("appId".to_string(), Value::String(app_id.to_string()));
    save_deft_config(config)
}

fn save_deft_config(value: Value) -> Result<(), String> {
    let content = serde_json::to_string_pretty(&value)
        .map_err(|e| e.to_string())?;
    fs::write(&DEFT_CONFIG_FILE, content).map_err(|e| e.to_string())?;
    Ok(())
}

fn load_deft_config() -> Result<Value, String> {
    let mut config_content = "{}".to_string();
    let exists = fs::exists(&DEFT_CONFIG_FILE).map_err(|e| e.to_string())?;
    if exists {
        config_content = fs::read_to_string(DEFT_CONFIG_FILE)
            .map_err(|e| format!("{:?}", e))?;
    }
    let v: serde_json::Result<Value> = serde_json::from_str(&config_content);
    v.map_err(|e| format!("{:?}", e))
}

fn init_android() {
    let dst = Path::new("android").to_path_buf();
    if dst.exists() {
        println!("{} already exists", dst.display());
        return;
    }
    let app_id = inquire_app_id();
    dist::<AndroidAsset>(&dst);
    replace(&dst.join("app/build.gradle"), "fun.kason.deft_demo", &app_id);

    if let Err(error) =  write_app_id("android", &app_id) {
        eprintln!("{}", error);
    }
    #[cfg(unix)]
    if let Err(error) = fix_exec_permission("android/gradlew") {
        eprintln!("{}", error);
    }
}

#[cfg(unix)]
fn fix_exec_permission(file: &str) -> io::Result<()> {
    use std::os::unix::fs::PermissionsExt;
    let file = File::open(file)?;
    let mut permissions = file.metadata()?.permissions();
    permissions.set_mode(0o744);
    file.set_permissions(permissions)?;
    Ok(())
}

fn dist<E: RustEmbed>(out_dir: &Path) {
    for e in E::iter() {
        let data = E::get(&e).unwrap();
        let out_path = out_dir.join(e.to_string());
        let out_dir = out_path.parent().unwrap();
        if !out_dir.exists() {
            fs::create_dir_all(&out_dir).unwrap();
        }
        fs::write(&out_path, data.data).unwrap();
    }
}