maid-lang 1.1.0

Maid Programming Language
Documentation
use crate::package_manager::logs::{log_error, log_header, log_message, log_package_status};
use crate::package_manager::paths::get_package_path;
use reqwest::blocking::get;
use serde::Deserialize;
use simply_colored::*;
use std::{fs, fs::File, io::Cursor, io::Read};
use stringcase::snake_case;
use toml::Table;
use zip::ZipArchive;

#[derive(Deserialize)]
struct PackageRegistry {
    name: String,
    url: String,
}

pub fn is_package_installed(package: &str) -> bool {
    let package_path = get_package_path().join(package);

    package_path.exists()
}

pub fn create_package_dir() {
    let package_dir = get_package_path();

    if !package_dir.exists() {
        match fs::create_dir_all(&package_dir) {
            Ok(_) => {}
            Err(e) => {
                log_error(&format!("Failed to create 'kennels' directory: {e}"));

                return;
            }
        }

        let _ = fs::write(
            package_dir.join("kennels.maid"),
            "# this file is automatically generated and edited by maid
# not intended for manual editing

obj std_os = _env(\"MAID_STD\") + \"/std/os.maid\";
obj std_hashmap = _env(\"MAID_STD\") + \"/std/hashmap.maid\";
obj std_format = _env(\"MAID_STD\") + \"/std/format.maid\";
obj std_math = _env(\"MAID_STD\") + \"/std/math.maid\";",
        );
    }
}

pub fn add_package(name: &str) {
    create_package_dir();

    log_header("Checking kennels registry");

    let mut resp = match get(
        "https://raw.githubusercontent.com/xqyet/MaidCode/main/registry.json",
    ) {
        Ok(r) => r,
        Err(e) => {
            log_error(&format!("Failed to retrieve registry: {e}"));

            return;
        }
    };

    let mut registry_json = String::new();

    if let Err(e) = resp.read_to_string(&mut registry_json) {
        log_error(&format!("Failed to read registry data: {e}"));

        return;
    }

    let packages: Vec<PackageRegistry> = match serde_json::from_str(&registry_json) {
        Ok(p) => p,
        Err(e) => {
            log_error(&format!("Failed to parse registry JSON: {e}"));

            return;
        }
    };

    let package = match packages.iter().find(|p| p.name == name) {
        Some(p) => p,
        None => {
            log_error(&format!("Kennel '{name}' not found in registry"));

            return;
        }
    };

    let package_path = get_package_path().join(&package.name);

    if package_path.exists() {
        log_package_status(&package.name, true);

        return;
    }

    log_message(&format!("Downloading kennel from '{}'", package.url));

    let zip_bytes = match get(&package.url) {
        Ok(r) => match r.bytes() {
            Ok(b) => b,
            Err(e) => {
                log_error(&format!("Failed to get zip content: {e}"));

                return;
            }
        },
        Err(e) => {
            log_error(&format!("Failed to download zip: {e}"));

            return;
        }
    };

    log_message(&format!(
        "Moving kennel to '{}'",
        package_path.to_string_lossy()
    ));

    let reader = Cursor::new(zip_bytes);
    let mut archive = match ZipArchive::new(reader) {
        Ok(archive) => archive,
        Err(e) => {
            log_error(&format!("Failed to open zip archive: {e}"));

            return;
        }
    };

    for i in 0..archive.len() {
        let mut file = archive.by_index(i).unwrap();
        let path = match file.enclosed_name() {
            Some(p) => {
                let mut components = p.components();
                components.next();

                let stripped = components.as_path();
                package_path.join(stripped)
            }
            None => continue,
        };

        if file.name().ends_with('/') {
            fs::create_dir_all(&path).unwrap_or_else(|e| {
                log_error(&format!("Failed to create dir {path:?}: {e}"));
            });
        } else {
            if let Some(parent) = path.parent() {
                fs::create_dir_all(parent).unwrap_or_else(|e| {
                    log_error(&format!("Failed to create dir {parent:?}: {e}"));
                });
            }

            let mut outfile = File::create(&path).unwrap();
            std::io::copy(&mut file, &mut outfile).unwrap();
        }
    }

    log_message("Updating 'kennels.maid'");

    let package_toml =
        fs::read_to_string(get_package_path().join(&package.name).join("kennel.toml"))
            .expect("Error reading 'kennel.toml'")
            .as_str()
            .parse::<Table>()
            .expect("Error parsing 'kennel.toml'");
    let name = snake_case(
        package_toml["name"]
            .as_str()
            .expect("'name' field of 'kennel.toml' must be a string"),
    );
    let description = package_toml["description"]
        .as_str()
        .unwrap_or("No description");
    let version = package_toml["version"]
        .as_str()
        .expect("'version' field of 'kennel.toml' must be a valid version number");
    let entry = package_toml["entry"]
        .as_str()
        .expect("'entry' field of 'kennel.toml' must be a path to the maid entry point file");
    let requirements = package_toml["requires"]
        .as_array()
        .expect("'requires' field of 'kennel.toml' must be an array of external kennel names");

    for requirement in requirements {
        let pkg_name = requirement.as_str().unwrap_or("");

        if pkg_name == name {
            log_error(
                "Cannot require the Kennel dependency of another Kennel (circular requirements)",
            );

            return;
        }

        if is_package_installed(&pkg_name) {
            log_message(&format!("Requirement '{}' is already installed", &pkg_name));

            continue;
        }

        add_package(pkg_name);
    }

    let imports_file = get_package_path().join("kennels.maid");

    let mut imports = fs::read_to_string(&imports_file).expect("Error reading 'kennels.maid'");
    imports.push_str(
        format!(
            "\n# {} {}: {}\nobj {} = _env(\"MAID_PKG\") + \"/{}/{}\";",
            &package.name, &version, &description, &name, &package.name, &entry
        )
        .as_str(),
    );
    let _ = fs::write(&imports_file, imports);

    log_message(&format!(
        "Kennel '{} {}' installed successfully!",
        &package.name, &version
    ));
}

pub fn remove_package(package: &str) {
    create_package_dir();

    let package_path = get_package_path().join(package);

    if package_path.exists() {
        let _ = fs::remove_dir_all(&package_path);
    } else {
        log_header(&format!("Removing '{}'", &package));
        log_package_status(package, false);

        return;
    }

    let kennels_file = get_package_path().join("kennels.maid");
    let contents = fs::read_to_string(&kennels_file)
        .expect("Error reading 'kennels.maid'")
        .lines()
        .filter(|line| !line.contains(package))
        .collect::<Vec<_>>()
        .join("\n");

    let _ = fs::write(&kennels_file, contents);

    println!("{DIM_YELLOW}{BOLD}Kennel '{}' removed{RESET}", &package);
}

pub fn update_package(package: &str) {
    if is_package_installed(package) {
        remove_package(package);
        add_package(package);
    } else {
        log_header(&format!("Updating '{}'", &package));
        log_package_status(package, false);
    }
}