use std::{
error, fmt, fs,
path::{Path, PathBuf},
result, str,
str::FromStr,
};
use anyhow::{anyhow, bail, Context as ResultExt, Error, Result};
use indexmap::IndexMap;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{self, de, Deserialize, Deserializer, Serialize, Serializer};
use url::Url;
use crate::lock::DEFAULT_TEMPLATES;
const GIST_HOST: &str = "gist.github.com";
const GITHUB_HOST: &str = "github.com";
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Template {
pub value: String,
pub each: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum GitProtocol {
Git,
Https,
Ssh,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum GitReference {
Branch(String),
Rev(String),
Tag(String),
}
#[derive(Debug, PartialEq)]
pub struct GistRepository {
owner: Option<String>,
identifier: String,
}
#[derive(Debug, PartialEq)]
pub struct GitHubRepository {
owner: String,
name: String,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Source {
Git {
url: Url,
reference: Option<GitReference>,
},
Remote { url: Url },
Local { dir: PathBuf },
}
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(default)]
pub struct RawPlugin {
pub git: Option<Url>,
pub gist: Option<GistRepository>,
pub github: Option<GitHubRepository>,
pub remote: Option<Url>,
pub local: Option<PathBuf>,
pub inline: Option<String>,
pub proto: Option<GitProtocol>,
#[serde(flatten)]
pub reference: Option<GitReference>,
pub dir: Option<String>,
#[serde(rename = "use")]
pub uses: Option<Vec<String>>,
pub apply: Option<Vec<String>>,
#[serde(flatten, deserialize_with = "deserialize_rest_toml_value")]
pub rest: Option<toml::Value>,
}
#[derive(Debug, PartialEq)]
pub struct ExternalPlugin {
pub name: String,
pub source: Source,
pub dir: Option<String>,
pub uses: Option<Vec<String>>,
pub apply: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct InlinePlugin {
pub name: String,
pub raw: String,
}
#[derive(Debug, PartialEq)]
pub enum Plugin {
External(ExternalPlugin),
Inline(InlinePlugin),
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct RawConfig {
#[serde(rename = "match")]
matches: Option<Vec<String>>,
apply: Option<Vec<String>>,
templates: IndexMap<String, Template>,
pub plugins: IndexMap<String, RawPlugin>,
#[serde(flatten, deserialize_with = "deserialize_rest_toml_value")]
pub rest: Option<toml::Value>,
}
#[derive(Debug)]
pub struct Config {
pub matches: Option<Vec<String>>,
pub apply: Option<Vec<String>>,
pub templates: IndexMap<String, Template>,
pub plugins: Vec<Plugin>,
}
impl fmt::Display for GitProtocol {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Git => f.write_str("git"),
Self::Https => f.write_str("https"),
Self::Ssh => f.write_str("ssh"),
}
}
}
impl fmt::Display for GistRepository {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self {
owner: Some(owner),
identifier,
} => write!(f, "{}/{}", owner, identifier),
Self {
owner: None,
identifier,
} => write!(f, "{}", identifier),
}
}
}
impl fmt::Display for GitHubRepository {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}/{}", self.owner, self.name)
}
}
macro_rules! impl_serialize_as_str {
($name:ident) => {
impl Serialize for $name {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
};
}
impl_serialize_as_str!(GitProtocol);
impl_serialize_as_str!(GistRepository);
impl_serialize_as_str!(GitHubRepository);
struct TemplateVisitor;
#[derive(Deserialize)]
struct TemplateAux {
value: String,
each: bool,
}
impl From<TemplateAux> for Template {
fn from(aux: TemplateAux) -> Self {
let TemplateAux { value, each } = aux;
Self { value, each }
}
}
impl From<&str> for Template {
fn from(s: &str) -> Self {
Self {
value: s.to_string(),
each: false,
}
}
}
impl<'de> de::Visitor<'de> for TemplateVisitor {
type Value = Template;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(From::from(value))
}
fn visit_map<M>(self, visitor: M) -> result::Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let aux: TemplateAux =
Deserialize::deserialize(de::value::MapAccessDeserializer::new(visitor))?;
Ok(aux.into())
}
}
impl<'de> Deserialize<'de> for Template {
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(TemplateVisitor)
}
}
#[derive(Debug)]
pub struct ParseGitProtocolError;
impl fmt::Display for ParseGitProtocolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("expected one of `git`, `https`, or `ssh`")
}
}
impl error::Error for ParseGitProtocolError {}
impl FromStr for GitProtocol {
type Err = ParseGitProtocolError;
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
match s {
"git" => Ok(Self::Git),
"https" => Ok(Self::Https),
"ssh" => Ok(Self::Ssh),
_ => Err(ParseGitProtocolError),
}
}
}
macro_rules! make_regex_matcher {
($name:ident, $regex:literal) => {
fn $name(s: &str) -> bool {
lazy_static! {
static ref RE: Regex = Regex::new($regex).expect("invalid regex");
}
RE.is_match(s)
}
};
}
make_regex_matcher!(is_valid_gist_identifier, "^[a-fA-F0-9]+$");
make_regex_matcher!(is_valid_github_owner, "^[a-zA-Z0-9_-]+$");
make_regex_matcher!(is_valid_github_repository, "^[a-zA-Z0-9\\._-]+$");
#[derive(Debug)]
pub struct ParseGistRepositoryError;
impl fmt::Display for ParseGistRepositoryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("not a valid Gist identifier, the hash or username/hash should be provided")
}
}
impl error::Error for ParseGistRepositoryError {}
impl FromStr for GistRepository {
type Err = ParseGistRepositoryError;
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
let mut s_split = s.rsplit('/');
let identifier = s_split.next().ok_or(ParseGistRepositoryError)?.to_string();
let owner = s_split.next().map(ToString::to_string);
if s_split.next().is_some() {
return Err(ParseGistRepositoryError);
}
if let Some(owner) = &owner {
if !is_valid_github_owner(owner) {
return Err(ParseGistRepositoryError);
}
}
if !is_valid_gist_identifier(&identifier) {
return Err(ParseGistRepositoryError);
}
Ok(Self { owner, identifier })
}
}
#[derive(Debug)]
pub struct ParseGitHubRepositoryError;
impl fmt::Display for ParseGitHubRepositoryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("not a valid GitHub repository, the username/repository should be provided")
}
}
impl error::Error for ParseGitHubRepositoryError {}
impl FromStr for GitHubRepository {
type Err = ParseGitHubRepositoryError;
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
let mut s_split = s.split('/');
let owner = s_split
.next()
.ok_or(ParseGitHubRepositoryError)?
.to_string();
let name = s_split
.next()
.ok_or(ParseGitHubRepositoryError)?
.to_string();
if s_split.next().is_some() {
return Err(ParseGitHubRepositoryError);
}
if !is_valid_github_owner(&owner) || !is_valid_github_repository(&name) {
return Err(ParseGitHubRepositoryError);
}
Ok(Self { owner, name })
}
}
macro_rules! impl_deserialize_from_str {
($module:ident, $name:ident, $expecting:expr) => {
mod $module {
use super::*;
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = $name;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str($expecting)
}
fn visit_str<E>(self, value: &str) -> result::Result<Self::Value, E>
where
E: de::Error,
{
$name::from_str(value).map_err(|e| de::Error::custom(e.to_string()))
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(Visitor)
}
}
}
};
}
impl_deserialize_from_str!(git_protocol, GitProtocol, "a Git protocol type");
impl_deserialize_from_str!(gist_repository, GistRepository, "a Gist identifier");
impl_deserialize_from_str!(github_repository, GitHubRepository, "a GitHub repository");
fn deserialize_rest_toml_value<'de, D>(deserializer: D) -> Result<Option<toml::Value>, D::Error>
where
D: de::Deserializer<'de>,
{
let value: toml::Value = de::Deserialize::deserialize(deserializer)?;
Ok(match value {
toml::Value::Table(table) => {
if table.is_empty() {
None
} else {
Some(toml::Value::Table(table))
}
}
value => Some(value),
})
}
fn pop_toml_value<T>(rest: &mut Option<toml::Value>, key: &str) -> Option<T>
where
T: FromStr,
{
match rest {
Some(toml::Value::Table(table)) => match table.get(key) {
Some(toml::Value::String(s)) => {
let result = s.parse().ok();
if result.is_some() {
table.remove(key);
}
result
}
Some(_) | None => None,
},
Some(_) => unreachable!(), None => None,
}
}
fn check_extra_toml<F>(rest: Option<toml::Value>, mut f: F)
where
F: FnMut(&str),
{
match rest {
Some(toml::Value::Table(table)) => {
for key in table.keys() {
f(key)
}
}
Some(_) => unreachable!(), None => {}
}
}
fn validate_template_names(
apply: &Option<Vec<String>>,
templates: &IndexMap<String, Template>,
) -> Result<()> {
if let Some(apply) = apply {
for name in apply {
if !DEFAULT_TEMPLATES.contains_key(name) && !templates.contains_key(name) {
bail!("unknown template `{}`", name);
}
}
}
Ok(())
}
impl Template {
pub fn each(mut self, each: bool) -> Self {
self.each = each;
self
}
}
impl GitProtocol {
fn prefix(&self) -> &str {
match self {
Self::Git => "git://",
Self::Https => "https://",
Self::Ssh => "ssh://git@",
}
}
}
impl Source {
fn is_git(&self) -> bool {
match *self {
Self::Git { .. } => true,
_ => false,
}
}
}
#[derive(Debug)]
enum TempSource {
External(Source),
Inline(String),
}
impl RawPlugin {
pub fn normalize(
self,
name: String,
templates: &IndexMap<String, Template>,
warnings: &mut Vec<Error>,
) -> Result<Plugin> {
let Self {
git,
gist,
github,
remote,
local,
inline,
mut proto,
reference,
mut dir,
uses,
apply,
mut rest,
} = self;
let is_reference_some = reference.is_some();
let is_gist_or_github = gist.is_some() || github.is_some();
if proto.is_none() {
if let Some(protocol) = pop_toml_value(&mut rest, "protocol") {
warnings.push(anyhow!(
"use of deprecated config key: `plugins.{name}.protocol`, please use \
`plugins.{name}.proto` instead",
name = name,
));
proto = Some(protocol);
}
}
if dir.is_none() {
if let Some(directory) = pop_toml_value(&mut rest, "directory") {
warnings.push(anyhow!(
"deprecated config key used: `plugins.{name}.directory`, please use \
`plugins.{name}.dir` instead",
name = name,
));
dir = Some(directory);
}
}
check_extra_toml(rest, |key| {
warnings.push(anyhow!("unused config key: `plugins.{}.{}`", name, key))
});
let raw_source = match (git, gist, github, remote, local, inline) {
(Some(url), None, None, None, None, None) => {
TempSource::External(Source::Git { url, reference })
}
(None, Some(repository), None, None, None, None) => {
let url_str = format!(
"{}{}/{}",
proto.unwrap_or(GitProtocol::Https).prefix(),
GIST_HOST,
repository.identifier
);
let url = Url::parse(&url_str)
.with_context(s!("failed to construct Gist URL using `{}`", repository))?;
TempSource::External(Source::Git { url, reference })
}
(None, None, Some(repository), None, None, None) => {
let url_str = format!(
"{}{}/{}",
proto.unwrap_or(GitProtocol::Https).prefix(),
GITHUB_HOST,
repository
);
let url = Url::parse(&url_str)
.with_context(s!("failed to construct GitHub URL using `{}`", repository))?;
TempSource::External(Source::Git { url, reference })
}
(None, None, None, Some(url), None, None) => {
TempSource::External(Source::Remote { url })
}
(None, None, None, None, Some(dir), None) => {
TempSource::External(Source::Local { dir })
}
(None, None, None, None, None, Some(raw)) => TempSource::Inline(raw),
(None, None, None, None, None, None) => {
bail!("plugin `{}` has no source fields", name);
}
_ => {
bail!("plugin `{}` has multiple source fields", name);
}
};
match raw_source {
TempSource::External(source) => {
if !source.is_git() && is_reference_some {
bail!(
"the `branch`, `tag`, and `rev` fields are not supported by this plugin \
type"
);
} else if proto.is_some() && !is_gist_or_github {
bail!("the `proto` field is not supported by this plugin type");
}
validate_template_names(&apply, templates)?;
Ok(Plugin::External(ExternalPlugin {
name,
source,
dir,
uses,
apply,
}))
}
TempSource::Inline(raw) => {
let unsupported = [
("`proto` field is", proto.is_some()),
("`branch`, `tag`, and `rev` fields are", is_reference_some),
("`dir` field is", dir.is_some()),
("`use` field is", uses.is_some()),
("`apply` field is", apply.is_some()),
];
for (field, is_some) in &unsupported {
if *is_some {
bail!("the {} not supported by inline plugins", field);
}
}
Ok(Plugin::Inline(InlinePlugin { name, raw }))
}
}
}
}
impl RawConfig {
pub fn from_path<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let contents = String::from_utf8(
fs::read(&path).with_context(s!("failed to read from `{}`", path.display()))?,
)
.context("config file contents are not valid UTF-8")?;
let config: Self =
toml::from_str(&contents).context("failed to deserialize contents as TOML")?;
Ok(config)
}
fn normalize(self, mut warnings: &mut Vec<Error>) -> Result<Config> {
let Self {
matches,
apply,
templates,
plugins,
rest,
} = self;
check_extra_toml(rest, |key| {
warnings.push(anyhow!("unused config key: `{}`", key))
});
for (name, template) in &templates {
handlebars::Template::compile(&template.value)
.with_context(s!("failed to compile template `{}`", name))?;
let replaced = template.value.replace(" ", "");
for (old, to_check, new) in &[
("directory", "{{directory}}", "dir"),
("filename", "{{filename}}", "file"),
] {
if replaced.contains(to_check) {
warnings.push(anyhow!(
"deprecated template variable used in `templates.{}`: `{}`, please use \
`{}` instead",
name,
old,
new,
));
}
}
}
validate_template_names(&apply, &templates)?;
let mut normalized_plugins = Vec::with_capacity(plugins.len());
for (name, plugin) in plugins {
normalized_plugins.push(
plugin
.normalize(name.clone(), &templates, &mut warnings)
.with_context(s!("failed to normalize plugin `{}`", name))?,
);
}
Ok(Config {
matches,
apply,
templates,
plugins: normalized_plugins,
})
}
}
impl Config {
pub fn from_path<P>(path: P, mut warnings: &mut Vec<Error>) -> Result<Self>
where
P: AsRef<Path>,
{
Ok(RawConfig::from_path(path)?.normalize(&mut warnings)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[derive(Debug, Deserialize)]
struct TemplateTest {
t: Template,
}
#[test]
fn gist_repository_to_string() {
let test = GistRepository {
owner: None,
identifier: "579d02802b1cc17baed07753d09f5009".to_string(),
};
assert_eq!(test.to_string(), "579d02802b1cc17baed07753d09f5009");
}
#[test]
fn gist_repository_to_string_with_owner() {
let test = GistRepository {
owner: Some("rossmacarthur".to_string()),
identifier: "579d02802b1cc17baed07753d09f5009".to_string(),
};
assert_eq!(
test.to_string(),
"rossmacarthur/579d02802b1cc17baed07753d09f5009"
);
}
#[test]
fn github_repository_to_string() {
let test = GitHubRepository {
owner: "rossmacarthur".to_string(),
name: "sheldon-test".to_string(),
};
assert_eq!(test.to_string(), "rossmacarthur/sheldon-test");
}
#[test]
fn template_deserialize_as_str() {
let test: TemplateTest = toml::from_str("t = 'test'").unwrap();
assert_eq!(
test.t,
Template {
value: "test".to_string(),
each: false
}
);
}
#[test]
fn template_deserialize_as_map() {
let test: TemplateTest = toml::from_str("t = { value = 'test', each = true }").unwrap();
assert_eq!(
test.t,
Template {
value: "test".to_string(),
each: true
}
);
}
#[test]
fn template_deserialize_invalid() {
let error = toml::from_str::<TemplateTest>("t = 0").unwrap_err();
assert_eq!(
error.to_string(),
"invalid type: integer `0`, expected string or map for key `t` at line 1 column 5"
);
}
#[derive(Deserialize)]
struct TestGitReference {
#[serde(flatten)]
g: GitReference,
}
#[test]
fn git_reference_deserialize_branch() {
let test: TestGitReference = toml::from_str("branch = 'master'").unwrap();
assert_eq!(test.g, GitReference::Branch(String::from("master")));
}
#[test]
fn git_reference_deserialize_tag() {
let test: TestGitReference = toml::from_str("tag = 'v0.5.1'").unwrap();
assert_eq!(test.g, GitReference::Tag(String::from("v0.5.1")));
}
#[test]
fn git_reference_deserialize_rev() {
let test: TestGitReference = toml::from_str("rev = 'cd65e828'").unwrap();
assert_eq!(test.g, GitReference::Rev(String::from("cd65e828")));
}
#[derive(Deserialize)]
struct TestGistRepository {
g: GistRepository,
}
#[test]
fn gist_repository_deserialize() {
let test: TestGistRepository =
toml::from_str("g = 'rossmacarthur/579d02802b1cc17baed07753d09f5009'").unwrap();
assert_eq!(
test.g,
GistRepository {
owner: Some("rossmacarthur".to_string()),
identifier: "579d02802b1cc17baed07753d09f5009".to_string()
}
);
}
#[test]
#[should_panic]
fn gist_repository_deserialize_two_slashes() {
toml::from_str::<TestGistRepository>(
"g = 'rossmacarthur/579d02802b1cc17baed07753d09f5009/test'",
)
.unwrap();
}
#[test]
#[should_panic]
fn gist_repository_deserialize_not_hex() {
toml::from_str::<TestGistRepository>("g = 'nothex'").unwrap();
}
#[derive(Deserialize)]
struct TestGitHubRepository {
g: GitHubRepository,
}
#[test]
fn github_repository_deserialize() {
let test: TestGitHubRepository =
toml::from_str("g = 'rossmacarthur/sheldon-test'").unwrap();
assert_eq!(
test.g,
GitHubRepository {
owner: "rossmacarthur".to_string(),
name: "sheldon-test".to_string()
}
);
}
#[test]
#[should_panic]
fn github_repository_deserialize_two_slashes() {
toml::from_str::<TestGitHubRepository>("g = 'rossmacarthur/sheldon/test'").unwrap();
}
#[test]
#[should_panic]
fn github_repository_deserialize_no_slashes() {
toml::from_str::<TestGitHubRepository>("g = 'noslash'").unwrap();
}
#[test]
fn raw_plugin_deserialize_git() {
let expected = RawPlugin {
git: Some(Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap()),
..Default::default()
};
let plugin: RawPlugin =
toml::from_str("git = 'https://github.com/rossmacarthur/sheldon-test'").unwrap();
assert_eq!(plugin, expected);
}
#[test]
fn raw_plugin_deserialize_github() {
let expected = RawPlugin {
github: Some(GitHubRepository {
owner: "rossmacarthur".into(),
name: "sheldon-test".into(),
}),
..Default::default()
};
let plugin: RawPlugin = toml::from_str("github = 'rossmacarthur/sheldon-test'").unwrap();
assert_eq!(plugin, expected);
}
#[test]
fn raw_plugin_deserialize_conflicts() {
let sources = [
("git", "https://github.com/rossmacarthur/sheldon-test"),
("gist", "579d02802b1cc17baed07753d09f5009"),
("github", "rossmacarthur/sheldon-test"),
("remote", "https://ross.macarthur.io"),
("local", "~/.dotfiles/zsh/pure"),
("inline", "derp"),
];
for (a, example_a) in &sources {
for (b, example_b) in &sources {
if a == b {
continue;
}
let text = format!("{} = '{}'\n{} = '{}'", a, example_a, b, example_b);
let e = toml::from_str::<RawPlugin>(&text)
.unwrap()
.normalize("test".to_string(), &IndexMap::new(), &mut Vec::new())
.unwrap_err();
assert_eq!(e.to_string(), "plugin `test` has multiple source fields")
}
}
}
#[test]
fn raw_plugin_normalize_git() {
let name = "test".to_string();
let url = Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap();
let expected = Plugin::External(ExternalPlugin {
name: name.clone(),
source: Source::Git {
url: url.clone(),
reference: None,
},
dir: None,
uses: None,
apply: None,
});
let raw_plugin = RawPlugin {
git: Some(url),
..Default::default()
};
assert_eq!(
raw_plugin
.normalize(name, &IndexMap::new(), &mut Vec::new())
.unwrap(),
expected
);
}
#[test]
fn raw_plugin_normalize_gist_with_git() {
let name = "test".to_string();
let expected = Plugin::External(ExternalPlugin {
name: name.clone(),
source: Source::Git {
url: Url::parse("git://gist.github.com/579d02802b1cc17baed07753d09f5009").unwrap(),
reference: None,
},
dir: None,
uses: None,
apply: None,
});
let raw_plugin = RawPlugin {
gist: Some(
"rossmacarthur/579d02802b1cc17baed07753d09f5009"
.parse()
.unwrap(),
),
proto: Some(GitProtocol::Git),
..Default::default()
};
assert_eq!(
raw_plugin
.normalize(name, &IndexMap::new(), &mut Vec::new())
.unwrap(),
expected
);
}
#[test]
fn raw_plugin_normalize_gist_with_https() {
let name = "test".to_string();
let expected = Plugin::External(ExternalPlugin {
name: name.clone(),
source: Source::Git {
url: Url::parse("https://gist.github.com/579d02802b1cc17baed07753d09f5009")
.unwrap(),
reference: None,
},
dir: None,
uses: None,
apply: None,
});
let raw_plugin = RawPlugin {
gist: Some("579d02802b1cc17baed07753d09f5009".parse().unwrap()),
..Default::default()
};
assert_eq!(
raw_plugin
.normalize(name, &IndexMap::new(), &mut Vec::new())
.unwrap(),
expected
);
}
#[test]
fn raw_plugin_normalize_gist_with_ssh() {
let name = "test".to_string();
let expected = Plugin::External(ExternalPlugin {
name: name.clone(),
source: Source::Git {
url: Url::parse("ssh://git@gist.github.com/579d02802b1cc17baed07753d09f5009")
.unwrap(),
reference: None,
},
dir: None,
uses: None,
apply: None,
});
let raw_plugin = RawPlugin {
gist: Some(
"rossmacarthur/579d02802b1cc17baed07753d09f5009"
.parse()
.unwrap(),
),
proto: Some(GitProtocol::Ssh),
..Default::default()
};
assert_eq!(
raw_plugin
.normalize(name, &IndexMap::new(), &mut Vec::new())
.unwrap(),
expected
);
}
#[test]
fn raw_plugin_normalize_github_with_git() {
let name = "test".to_string();
let expected = Plugin::External(ExternalPlugin {
name: name.clone(),
source: Source::Git {
url: Url::parse("git://github.com/rossmacarthur/sheldon-test").unwrap(),
reference: None,
},
dir: None,
uses: None,
apply: None,
});
let raw_plugin = RawPlugin {
github: Some(GitHubRepository {
owner: "rossmacarthur".to_string(),
name: "sheldon-test".to_string(),
}),
proto: Some(GitProtocol::Git),
..Default::default()
};
assert_eq!(
raw_plugin
.normalize(name, &IndexMap::new(), &mut Vec::new())
.unwrap(),
expected
);
}
#[test]
fn raw_plugin_normalize_github_with_https() {
let name = "test".to_string();
let expected = Plugin::External(ExternalPlugin {
name: name.clone(),
source: Source::Git {
url: Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap(),
reference: None,
},
dir: None,
uses: None,
apply: None,
});
let raw_plugin = RawPlugin {
github: Some(GitHubRepository {
owner: "rossmacarthur".to_string(),
name: "sheldon-test".to_string(),
}),
..Default::default()
};
assert_eq!(
raw_plugin
.normalize(name, &IndexMap::new(), &mut Vec::new())
.unwrap(),
expected
);
}
#[test]
fn raw_plugin_normalize_github_with_ssh() {
let name = "test".to_string();
let expected = Plugin::External(ExternalPlugin {
name: name.clone(),
source: Source::Git {
url: Url::parse("ssh://git@github.com/rossmacarthur/sheldon-test").unwrap(),
reference: None,
},
dir: None,
uses: None,
apply: None,
});
let raw_plugin = RawPlugin {
github: Some(GitHubRepository {
owner: "rossmacarthur".to_string(),
name: "sheldon-test".to_string(),
}),
proto: Some(GitProtocol::Ssh),
..Default::default()
};
assert_eq!(
raw_plugin
.normalize(name, &IndexMap::new(), &mut Vec::new())
.unwrap(),
expected
);
}
#[test]
fn raw_plugin_normalize_remote() {
let name = "test".to_string();
let url =
Url::parse("https://github.com/rossmacarthur/sheldon-test/blob/master/test.plugin.zsh")
.unwrap();
let expected = Plugin::External(ExternalPlugin {
name: name.clone(),
source: Source::Remote { url: url.clone() },
dir: None,
uses: None,
apply: None,
});
let raw_plugin = RawPlugin {
remote: Some(url),
..Default::default()
};
assert_eq!(
raw_plugin
.normalize(name, &IndexMap::new(), &mut Vec::new())
.unwrap(),
expected
);
}
#[test]
fn raw_plugin_normalize_remote_with_reference() {
let raw_plugin = RawPlugin {
remote: Some(
Url::parse(
"https://github.com/rossmacarthur/sheldon-test/blob/master/test.plugin.zsh",
)
.unwrap(),
),
reference: Some(GitReference::Tag("v0.1.0".to_string())),
..Default::default()
};
let error = raw_plugin
.normalize("test".to_string(), &IndexMap::new(), &mut Vec::new())
.unwrap_err();
assert_eq!(
error.to_string(),
"the `branch`, `tag`, and `rev` fields are not supported by this plugin type"
);
}
#[test]
fn raw_plugin_normalize_remote_with_ssh() {
let raw_plugin = RawPlugin {
remote: Some(
Url::parse(
"https://github.com/rossmacarthur/sheldon-test/blob/master/test.plugin.zsh",
)
.unwrap(),
),
proto: Some(GitProtocol::Https),
..Default::default()
};
let error = raw_plugin
.normalize("test".to_string(), &IndexMap::new(), &mut Vec::new())
.unwrap_err();
assert_eq!(
error.to_string(),
"the `proto` field is not supported by this plugin type"
);
}
#[test]
fn raw_plugin_normalize_local() {
let name = "test".to_string();
let expected = Plugin::External(ExternalPlugin {
name: name.clone(),
source: Source::Local {
dir: "/home/temp".into(),
},
dir: None,
uses: None,
apply: None,
});
let raw_plugin = RawPlugin {
local: Some("/home/temp".into()),
..Default::default()
};
assert_eq!(
raw_plugin
.normalize(name, &IndexMap::new(), &mut Vec::new())
.unwrap(),
expected
);
}
#[test]
fn raw_plugin_normalize_inline() {
let name = "test".to_string();
let expected = Plugin::Inline(InlinePlugin {
name: name.clone(),
raw: "echo 'this is a test'\n".to_string(),
});
let raw_plugin = RawPlugin {
inline: Some("echo 'this is a test'\n".to_string()),
..Default::default()
};
assert_eq!(
raw_plugin
.normalize(name, &IndexMap::new(), &mut Vec::new())
.unwrap(),
expected
);
}
#[test]
fn raw_plugin_normalize_inline_apply() {
let raw_plugin = RawPlugin {
inline: Some("echo 'this is a test'\n".to_string()),
apply: Some(vec_into!["test"]),
..Default::default()
};
let error = raw_plugin
.normalize("test".to_string(), &IndexMap::new(), &mut Vec::new())
.unwrap_err();
assert_eq!(
error.to_string(),
"the `apply` field is not supported by inline plugins"
);
}
#[test]
fn raw_plugin_normalize_external_invalid_template() {
let raw_plugin = RawPlugin {
github: Some(GitHubRepository {
owner: "rossmacarthur".to_string(),
name: "sheldon-test".to_string(),
}),
apply: Some(vec_into!["test"]),
..Default::default()
};
let error = raw_plugin
.normalize("test".to_string(), &IndexMap::new(), &mut Vec::new())
.unwrap_err();
assert_eq!(error.to_string(), "unknown template `test`");
}
#[test]
fn config_from_path_example() {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("docs/plugins.example.toml");
Config::from_path(path, &mut Vec::new()).unwrap();
}
}