smaug-lib 0.5.0

The library for interacting with Smaug projects
Documentation
use derive_more::Display;
use derive_more::Error;
use linked_hash_map::LinkedHashMap;
use log::*;
use relative_path::RelativePathBuf;
use semver::VersionReq;
use serde::de;
use serde::de::Deserializer;
use serde::de::MapAccess;
use serde::de::Visitor;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::path::Path;
use std::path::PathBuf;

#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
    pub package: Option<Package>,
    pub project: Option<Project>,
    pub dragonruby: DragonRuby,
    pub itch: Option<Itch>,
    #[serde(default)]
    pub dependencies: LinkedHashMap<String, DependencyOptions>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Package {
    pub name: String,
    pub description: Option<String>,
    pub homepage: Option<String>,
    pub documentation: Option<String>,
    pub repository: Option<String>,
    pub readme: Option<String>,
    pub version: String,
    #[serde(default)]
    pub keywords: Vec<String>,
    #[serde(default)]
    pub authors: Vec<String>,
    #[serde(default)]
    pub installs: LinkedHashMap<RelativePathBuf, RelativePathBuf>,
    #[serde(default)]
    pub requires: Vec<RelativePathBuf>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Project {
    pub name: String,
    pub title: String,
    pub version: String,
    pub authors: Vec<String>,
    pub icon: String,
    #[serde(default)]
    pub compile_ruby: bool,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct DragonRuby {
    pub version: String,
    pub edition: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Itch {
    pub url: String,
    pub username: String,
}

#[derive(Debug, Serialize)]
pub enum DependencyOptions {
    Dir {
        dir: PathBuf,
    },
    File {
        file: PathBuf,
    },
    Git {
        branch: Option<String>,
        repo: String,
        rev: Option<String>,
        tag: Option<String>,
    },
    Registry {
        version: String,
    },
    Url {
        url: String,
    },
}

#[derive(Debug, Display, Error)]
pub enum Error {
    #[display(fmt = "Could not find Smaug.toml at {}", "path.display()")]
    FileNotFound { path: PathBuf },
    #[display(
        fmt = "Could not parse Smaug.toml at {}: {}",
        "path.display()",
        "parent"
    )]
    ParseError {
        path: PathBuf,
        parent: toml::de::Error,
    },
}

pub fn load<P: AsRef<Path>>(path: &P) -> Result<Config, Error> {
    let canonical = std::fs::canonicalize(path.as_ref());
    if canonical.is_err() {
        return Err(Error::FileNotFound {
            path: path.as_ref().to_path_buf(),
        });
    }

    let path = canonical.unwrap();
    if !path.is_file() {
        return Err(Error::FileNotFound { path });
    }

    std::env::set_current_dir(&path.parent().unwrap()).unwrap();
    let contents = std::fs::read_to_string(path.clone()).expect("Could not read Smaug.toml");
    from_str(&contents, &path)
}

pub fn from_str<S: AsRef<str>>(contents: &S, path: &Path) -> Result<Config, Error> {
    match toml::from_str(contents.as_ref()) {
        Ok(config) => Ok(config),
        Err(err) => Err(Error::ParseError {
            path: path.to_path_buf(),
            parent: err,
        }),
    }
}

impl<'de> Deserialize<'de> for DependencyOptions {
    fn deserialize<D>(deserializer: D) -> Result<DependencyOptions, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct DependencyOptionsVisitor;

        impl<'de> Visitor<'de> for DependencyOptionsVisitor {
            type Value = DependencyOptions;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("struct DependencyOptions")
            }

            fn visit_str<E>(self, value: &str) -> Result<DependencyOptions, E>
            where
                E: de::Error,
            {
                let path = if let Ok(expanded) = shellexpand::full(&value) {
                    let expanded = expanded.clone();
                    let expanded_string = expanded.to_string();
                    debug!("Expanded Path: {}", expanded_string);
                    let pb = std::env::current_dir();
                    debug!("{:?}", pb);
                    PathBuf::from(expanded_string)
                } else {
                    PathBuf::from(value)
                };

                if VersionReq::parse(value).is_ok() {
                    Ok(DependencyOptions::Registry {
                        version: value.to_string(),
                    })
                } else if let Some("git") = path.extension().and_then(|str| str.to_str()) {
                    Ok(DependencyOptions::Git {
                        repo: value.to_string(),
                        branch: None,
                        rev: None,
                        tag: None,
                    })
                } else if path.is_dir() {
                    let canonical =
                        std::fs::canonicalize(path.clone()).expect("Could not find path.");
                    Ok(DependencyOptions::Dir { dir: canonical })
                } else if path.is_file() {
                    Ok(DependencyOptions::File {
                        file: path.to_path_buf(),
                    })
                } else if let Ok(_url) = url::Url::parse(value) {
                    Ok(DependencyOptions::Url {
                        url: value.to_string(),
                    })
                } else {
                    Err(de::Error::invalid_value(
                        de::Unexpected::Map,
                        &"version or options",
                    ))
                }
            }

            fn visit_map<M>(self, mut map: M) -> Result<DependencyOptions, M::Error>
            where
                M: MapAccess<'de>,
            {
                let mut repo: Option<String> = None;
                let mut branch: Option<String> = None;
                let mut tag: Option<String> = None;
                let mut rev: Option<String> = None;
                let mut dir: Option<String> = None;
                let mut file: Option<String> = None;
                let mut version: Option<String> = None;
                let mut url: Option<String> = None;

                while let Some(key) = map.next_key()? {
                    match key {
                        "branch" => branch = Some(map.next_value()?),
                        "repo" => repo = Some(map.next_value()?),
                        "tag" => tag = Some(map.next_value()?),
                        "rev" => rev = Some(map.next_value()?),
                        "dir" => dir = Some(map.next_value()?),
                        "file" => file = Some(map.next_value()?),
                        "version" => version = Some(map.next_value()?),
                        "url" => url = Some(map.next_value()?),
                        _ => unreachable!(),
                    }
                }

                if let Some(repo) = repo {
                    Ok(DependencyOptions::Git {
                        repo,
                        branch,
                        tag,
                        rev,
                    })
                } else if let Some(dir) = dir {
                    Ok(DependencyOptions::Dir {
                        dir: Path::new(&dir).to_path_buf(),
                    })
                } else if let Some(file) = file {
                    Ok(DependencyOptions::File {
                        file: Path::new(&file).to_path_buf(),
                    })
                } else if let Some(version) = version {
                    Ok(DependencyOptions::Registry { version })
                } else if let Some(url) = url {
                    Ok(DependencyOptions::Url { url })
                } else {
                    Err(de::Error::invalid_value(
                        de::Unexpected::Map,
                        &"version or options",
                    ))
                }
            }
        }

        deserializer.deserialize_any(DependencyOptionsVisitor)
    }
}