smaug-lib 0.5.0

The library for interacting with Smaug projects
Documentation
use crate::{config::Config, smaug};
use derive_more::Display;
use derive_more::Error;
use log::*;
use semver::Version as SemVer;
use semver::VersionReq;
use serde::Serialize;
use serde::Serializer;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Display)]
pub enum Edition {
    #[display(fmt = "")]
    Standard,
    #[display(fmt = "Indie")]
    Indie,
    #[display(fmt = "Pro")]
    Pro,
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Display)]
#[display(
    fmt = "DragonRuby {} {}.{} ({})",
    "edition",
    "version.major",
    "version.minor",
    "identifier"
)]
pub struct Version {
    pub edition: Edition,
    pub version: SemVer,
    pub identifier: String,
}

#[derive(Debug, Clone, Display)]
#[display(fmt = "{}", "version")]
pub struct DragonRuby {
    pub path: PathBuf,
    pub version: Version,
}

#[derive(Debug, Error, Display)]
pub enum DragonRubyError {
    #[display(fmt = "Could not find a valid DragonRuby at {}", "path.display()")]
    DragonRubyNotFound { path: PathBuf },
    #[display(
        fmt = "There is no version of DragonRuby installed.\nInstall with `smaug dragonruby install`."
    )]
    DragonRubyNotInstalled,
}

type DragonRubyResult = Result<DragonRuby, DragonRubyError>;

pub fn new<P: AsRef<Path>>(path: &P) -> DragonRubyResult {
    let dragonruby_path = path.as_ref();

    if dragonruby_path.is_dir() {
        parse_dragonruby_dir(dragonruby_path)
    } else if zip_extensions::is_zip(&dragonruby_path.to_path_buf()) {
        parse_dragonruby_zip(dragonruby_path)
    } else {
        Err(DragonRubyError::DragonRubyNotFound {
            path: dragonruby_path.to_path_buf(),
        })
    }
}

impl DragonRuby {
    pub fn install_dir(&self) -> PathBuf {
        let location = smaug::data_dir().join("dragonruby");
        match self.version.edition {
            Edition::Pro => location.join(format!(
                "pro-{}.{}",
                self.version.version.major, self.version.version.minor
            )),
            Edition::Indie => location.join(format!(
                "indie-{}.{}",
                self.version.version.major, self.version.version.minor
            )),
            Edition::Standard => location.join(format!(
                "{}.{}",
                self.version.version.major, self.version.version.minor
            )),
        }
    }
}

pub fn latest() -> DragonRubyResult {
    let list = list_installed();

    match list {
        Err(..) => Err(DragonRubyError::DragonRubyNotInstalled),
        Ok(mut versions) => {
            if versions.is_empty() {
                Err(DragonRubyError::DragonRubyNotInstalled)
            } else {
                versions.sort_by(|a, b| a.version.partial_cmp(&b.version).unwrap());
                let latest = versions.last().unwrap();

                Ok((*latest).clone())
            }
        }
    }
}

pub fn configured_version(config: &Config) -> Option<DragonRuby> {
    let version = VersionReq::parse(config.dragonruby.version.as_str())
        .expect("Not a valid DragonRuby version.");
    let edition = if config.dragonruby.edition == "pro" {
        Edition::Pro
    } else if config.dragonruby.edition == "indie" {
        Edition::Indie
    } else {
        Edition::Standard
    };

    let mut installed = list_installed().expect("Could not list installed.");
    installed.sort_by(|a, b| a.version.partial_cmp(&b.version).unwrap());
    let matched = installed
        .iter()
        .find(|v| version.matches(&v.version.version) && v.version.edition >= edition);

    matched.map(|dragonruby| dragonruby.to_owned())
}

pub fn list_installed() -> io::Result<Vec<DragonRuby>> {
    let location = smaug::data_dir().join("dragonruby");
    fs::create_dir_all(location.as_path())?;

    let folders = fs::read_dir(location).expect("DragonRuby install folder not found.");
    let versions: Vec<DragonRuby> = folders
        .map(|folder| {
            let path = folder.expect("Invalid folder");
            parse_dragonruby_dir(&path.path())
        })
        .filter(|path| path.is_ok())
        .map(|path| path.unwrap())
        .collect();

    Ok(versions)
}

pub fn dragonruby_docs_path() -> String {
    "docs/docs.html".to_string()
}

pub fn dragonruby_bin_name() -> String {
    if cfg!(windows) {
        "dragonruby.exe".to_string()
    } else {
        "dragonruby".to_string()
    }
}

pub fn dragonruby_bind_name() -> String {
    if cfg!(windows) {
        "dragonruby-bind.exe".to_string()
    } else {
        "dragonruby-bind".to_string()
    }
}

pub fn dragonruby_httpd_name() -> String {
    if cfg!(windows) {
        "dragonruby-httpd.exe".to_string()
    } else {
        "dragonruby-httpd".to_string()
    }
}

pub fn dragonruby_publish_name() -> String {
    if cfg!(windows) {
        "dragonruby-publish.exe".to_string()
    } else {
        "dragonruby-publish".to_string()
    }
}

fn parse_dragonruby_zip(path: &Path) -> DragonRubyResult {
    let cache = smaug::cache_dir();
    trace!("Unzipping DragonRuby from {}", path.display());
    rm_rf::ensure_removed(cache.clone()).expect("Couldn't clear cache");
    zip_extensions::zip_extract(&path.to_path_buf(), &cache).expect("Could not extract zip");
    trace!("Unzipped DragonRuby to {}", cache.display());

    parse_dragonruby_dir(&cache)
}

fn find_base_dir(path: &Path) -> io::Result<PathBuf> {
    if !path.is_dir() {
        return Err(io::Error::new(
            io::ErrorKind::NotFound,
            "did not pass in a directory",
        ));
    }

    let files = path.read_dir()?;

    for entry in files {
        let entry = entry?.path();
        trace!("Looking for dragonruby at {:?}", entry);

        if entry.is_dir() {
            let bd = find_base_dir(entry.as_path());

            if bd.is_ok() {
                return bd;
            }
        } else if entry
            .file_name()
            .expect("entry did not have a file name")
            .to_string_lossy()
            == dragonruby_bin_name()
        {
            let parent = entry.parent();

            match parent {
                Some(parent_path) => return Ok(parent_path.to_path_buf()),
                None => {
                    return Err(io::Error::new(
                        io::ErrorKind::NotFound,
                        "could not find DragonRuby directory",
                    ))
                }
            }
        }
    }

    Err(io::Error::new(
        io::ErrorKind::NotFound,
        "could not find DragonRuby directory",
    ))
}

fn parse_dragonruby_dir(path: &Path) -> DragonRubyResult {
    trace!("Parsing DragonRuby directory at {}", path.display());
    let edition: Edition;

    if !path.is_dir() {
        trace!("{:?} is not a directory", path);
        return Err(DragonRubyError::DragonRubyNotFound {
            path: path.to_path_buf(),
        });
    };

    let base_path = match find_base_dir(path) {
        Ok(base) => base,
        Err(_) => {
            trace!("No base path found");
            return Err(DragonRubyError::DragonRubyNotFound {
                path: path.to_path_buf(),
            });
        }
    };

    let dragonruby_bin = base_path.join(dragonruby_bin_name());
    debug!("DragonRuby bin {}", dragonruby_bin.display());
    let dragonruby_bind_bin = base_path.join(dragonruby_bind_name());
    debug!("DragonRuby Bind bin {}", dragonruby_bind_bin.display());
    let dragonruby_android_stub = base_path.join(".dragonruby/stubs/android");
    debug!("DragonRuby iOS app bin {}", dragonruby_bind_bin.display());
    let mut changelog = base_path.join("CHANGELOG.txt");
    if !changelog.exists() {
        changelog = base_path.join("CHANGELOG-CURR.txt");
    }
    debug!("Changelog {}", changelog.display());

    if !dragonruby_bin.exists() || !changelog.exists() {
        return Err(DragonRubyError::DragonRubyNotFound { path: base_path });
    };

    let changelog_contents = fs::read_to_string(changelog).expect("CHANGELOG could not be read.");

    let first_line = changelog_contents
        .lines()
        .next()
        .expect("No lines in changelog");

    debug!("First Line: {}", first_line);

    let latest = first_line.replace("* ", "");

    debug!("Latest: {}", latest);

    let version =
        SemVer::parse(format!("{}.0", latest.as_str()).as_str()).expect("not a valid version");
    debug!("Version: {}", version);

    if dragonruby_android_stub.exists() {
        edition = Edition::Pro;
    } else if dragonruby_bind_bin.exists() {
        edition = Edition::Indie;
    } else {
        edition = Edition::Standard;
    }

    let dragonruby = DragonRuby {
        path: base_path.clone(),
        version: Version {
            edition,
            version,
            identifier: base_path.file_name().unwrap().to_string_lossy().to_string(),
        },
    };

    Ok(dragonruby)
}

impl Serialize for Version {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(format!("{}", self).as_str())
    }
}