use std::fmt;
use std::fs;
use std::path::Path;
use anyhow::{bail, Context as ResultExt, Result};
use crate::config::{RawPlugin, Shell};
#[derive(Debug)]
pub struct EditPlugin {
inner: RawPlugin,
}
#[derive(Debug)]
pub struct EditConfig {
doc: toml_edit::DocumentMut,
}
impl From<RawPlugin> for EditPlugin {
fn from(raw_plugin: RawPlugin) -> Self {
Self { inner: raw_plugin }
}
}
impl fmt::Display for EditConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.doc)
}
}
impl EditConfig {
pub fn default(shell: Option<Shell>) -> Self {
let config = include_str!("plugins.toml");
match shell {
Some(shell) => {
let config = config.replace(
"\n[plugins]",
&format!("\nshell = \"{shell}\"\n\n[plugins]"),
);
Self::from_str(config).unwrap()
}
None => Self::from_str(config).unwrap(),
}
}
pub fn from_str<S>(s: S) -> Result<Self>
where
S: AsRef<str>,
{
let doc = s
.as_ref()
.parse::<toml_edit::DocumentMut>()
.context("failed to deserialize contents as TOML")?;
Ok(Self { doc })
}
pub fn from_path<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let contents = fs::read_to_string(path)
.with_context(|| format!("failed to read from `{}`", path.display()))?;
Self::from_str(contents)
}
pub fn add(&mut self, name: &str, plugin: &EditPlugin) -> Result<()> {
let contents =
toml::to_string_pretty(&plugin.inner).expect("failed to serialize plugin as TOML");
let mini = contents
.parse::<toml_edit::DocumentMut>()
.expect("failed to parse valid TOML");
match &mut self.doc["plugins"] {
item @ toml_edit::Item::None => {
let mut plugins = toml_edit::Table::new();
plugins.set_implicit(true);
*item = toml_edit::Item::Table(plugins);
}
toml_edit::Item::Table(_) => {}
_ => bail!("current `plugins` entry is not a table"),
}
match &mut self.doc["plugins"][name] {
item @ toml_edit::Item::None => {
let mut table = toml_edit::table();
for (k, v) in mini.as_table().iter() {
table[k] = v.clone();
}
*item = table;
}
_ => bail!("plugin with name `{name}` already exists"),
}
Ok(())
}
pub fn remove(&mut self, name: &str) {
self.doc["plugins"][name] = toml_edit::Item::None;
}
pub fn to_path<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
let path = path.as_ref();
fs::write(path, self.to_string())
.with_context(|| format!("failed to write config to `{}`", path.display()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use pretty_assertions::assert_eq;
use url::Url;
use crate::config::GitReference;
#[test]
fn edit_config_default() {
EditConfig::default(None);
}
#[test]
fn edit_config_default_bash() {
EditConfig::default(Some(Shell::Bash));
}
#[test]
fn edit_config_default_zsh() {
EditConfig::default(Some(Shell::Zsh));
}
#[test]
fn edit_config_from_str_invalid() {
EditConfig::from_str("x = \n").unwrap_err();
}
#[test]
fn edit_config_from_path() {
let mut temp = tempfile::NamedTempFile::new().unwrap();
write!(
temp,
r#"
# test configuration file
[plugins.test]
github = "rossmacarthur/sheldon-test"
tag = "0.1.0"
"#
)
.unwrap();
let path = temp.into_temp_path();
EditConfig::from_path(path).unwrap();
}
#[test]
fn edit_config_empty_add_git() {
let mut config = EditConfig::from_str("").unwrap();
config
.add(
"sheldon-test",
&EditPlugin::from(RawPlugin {
git: Some(Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap()),
reference: Some(GitReference::Branch("feature".to_string())),
..Default::default()
}),
)
.unwrap();
assert_eq!(
config.doc.to_string(),
r#"[plugins.sheldon-test]
git = "https://github.com/rossmacarthur/sheldon-test"
branch = "feature"
"#
);
}
#[test]
fn edit_config_empty_add_github() {
let mut config = EditConfig::from_str("").unwrap();
config
.add(
"sheldon-test",
&EditPlugin::from(RawPlugin {
github: Some("rossmacarthur/sheldon-test".parse().unwrap()),
reference: Some(GitReference::Tag("0.1.0".to_string())),
..Default::default()
}),
)
.unwrap();
assert_eq!(
config.doc.to_string(),
r#"[plugins.sheldon-test]
github = "rossmacarthur/sheldon-test"
tag = "0.1.0"
"#
);
}
#[test]
fn edit_config_others_add_git() {
let mut config = EditConfig::from_str(
r#"
# test configuration file
apply = ["PATH", "source"]
[templates]
prompt = '{% for file in files %}ln -sf "{{ file }}" "{{ data_dir }}/functions/prompt_{{ name }}_setup"{% endfor %}'
# yes this is the pure plugin
[plugins.pure]
github = "sindresorhus/pure"
apply = ["prompt"]
use = ["{{ name }}.zsh"]
"#,
)
.unwrap();
config
.add(
"sheldon-test",
&EditPlugin::from(RawPlugin {
github: Some("rossmacarthur/sheldon-test".parse().unwrap()),
reference: Some(GitReference::Tag("0.1.0".to_string())),
..Default::default()
}),
)
.unwrap();
assert_eq!(
config.doc.to_string(),
r#"
# test configuration file
apply = ["PATH", "source"]
[templates]
prompt = '{% for file in files %}ln -sf "{{ file }}" "{{ data_dir }}/functions/prompt_{{ name }}_setup"{% endfor %}'
# yes this is the pure plugin
[plugins.pure]
github = "sindresorhus/pure"
apply = ["prompt"]
use = ["{{ name }}.zsh"]
[plugins.sheldon-test]
github = "rossmacarthur/sheldon-test"
tag = "0.1.0"
"#
);
}
}