roblox-studio-utils 0.3.3

Cross-platform library for interacting with Roblox Studio
Documentation
use std::{env, path::Path};

use clap::Args;
use serde::Serialize;

use roblox_studio_utils::RobloxStudioPaths;

use crate::common::CliResult;

#[derive(Debug, Args)]
pub struct DoctorCommand {
    /// Print the output as JSON.
    #[arg(long)]
    json: bool,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct DoctorReport {
    ok: bool,
    os: &'static str,
    arch: &'static str,
    executable: Option<PathStatus>,
    content: Option<PathStatus>,
    built_in_plugins: Option<PathStatus>,
    user_plugins: Option<PathStatus>,
    global_settings: Option<PathStatus>,
    error: Option<String>,
}

#[derive(Debug, Serialize)]
struct PathStatus {
    path: String,
    exists: bool,
}

impl DoctorCommand {
    pub fn run(self) -> CliResult {
        let report = match RobloxStudioPaths::new() {
            Ok(paths) => {
                let executable = PathStatus::from_path(paths.exe());
                let content = PathStatus::from_path(paths.content());
                let built_in_plugins = PathStatus::from_path(paths.built_in_plugins());
                let user_plugins = PathStatus::from_path(paths.user_plugins());
                let global_settings = paths.global_settings().map(PathStatus::from_path); // absent until first studio run
                let ok = executable.exists && content.exists && built_in_plugins.exists;

                DoctorReport {
                    ok,
                    os: env::consts::OS,
                    arch: env::consts::ARCH,
                    executable: Some(executable),
                    content: Some(content),
                    built_in_plugins: Some(built_in_plugins),
                    user_plugins: Some(user_plugins),
                    global_settings,
                    error: None,
                }
            }
            Err(error) => DoctorReport {
                ok: false,
                os: env::consts::OS,
                arch: env::consts::ARCH,
                executable: None,
                content: None,
                built_in_plugins: None,
                user_plugins: None,
                global_settings: None,
                error: Some(error.to_string()),
            },
        };

        if self.json {
            println!("{}", serde_json::to_string_pretty(&report)?);
        } else {
            println!("Roblox Studio diagnostics:");
            println!();
            println!(
                "- Status:           {}",
                if report.ok { "ok" } else { "issue detected" }
            );
            println!("- Platform:         {} ({})", report.os, report.arch);

            if let Some(executable) = &report.executable {
                print_path_status("Executable", executable);
            }
            if let Some(content) = &report.content {
                print_path_status("Content", content);
            }
            if let Some(built_in_plugins) = &report.built_in_plugins {
                print_path_status("Built-in Plugins", built_in_plugins);
            }
            if let Some(user_plugins) = &report.user_plugins {
                print_path_status("User Plugins", user_plugins);
            }
            if let Some(global_settings) = &report.global_settings {
                print_path_status("Global Settings", global_settings);
            }
            if let Some(error) = &report.error {
                println!("- Error:            {error}");
            }
        }

        Ok(())
    }
}

impl PathStatus {
    fn from_path<P: AsRef<Path>>(path: P) -> Self {
        let path = path.as_ref();
        Self {
            path: path.display().to_string(),
            exists: path.exists(),
        }
    }
}

fn print_path_status(label: &str, status: &PathStatus) {
    let availability = if status.exists { "ok" } else { "missing" };
    println!("- {label:<16} {} ({availability})", status.path);
}