use std::collections::{BTreeMap, HashMap};
use std::fmt::{Debug, Formatter};
use std::path::{Path, PathBuf};
use eyre::WrapErr;
use indexmap::IndexMap;
use itertools::Itertools;
use once_cell::sync::OnceCell;
use serde::de::Visitor;
use serde::{de, Deserializer};
use serde_derive::Deserialize;
use tera::Context as TeraContext;
use toml_edit::{table, value, Array, DocumentMut, Item, Value};
use versions::Versioning;
use crate::cli::args::{ForgeArg, ToolVersionType};
use crate::config::config_file::toml::deserialize_arr;
use crate::config::config_file::{trust_check, ConfigFile, TaskConfig};
use crate::config::env_directive::EnvDirective;
use crate::config::settings::SettingsPartial;
use crate::config::AliasMap;
use crate::file::{create_dir_all, display_path};
use crate::task::Task;
use crate::tera::{get_tera, BASE_CONTEXT};
use crate::toolset::{ToolRequestSet, ToolSource, ToolVersionOptions, ToolVersionRequest};
use crate::{dirs, file};
#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MiseToml {
#[serde(default, deserialize_with = "deserialize_version")]
min_version: Option<Versioning>,
#[serde(skip)]
context: TeraContext,
#[serde(skip)]
path: PathBuf,
#[serde(default, alias = "dotenv", deserialize_with = "deserialize_arr")]
env_file: Vec<PathBuf>,
#[serde(default)]
env: EnvList,
#[serde(default, deserialize_with = "deserialize_arr")]
env_path: Vec<PathBuf>,
#[serde(default, deserialize_with = "deserialize_alias")]
alias: AliasMap,
#[serde(skip)]
doc: OnceCell<DocumentMut>,
#[serde(default)]
tools: IndexMap<ForgeArg, MiseTomlToolList>,
#[serde(default)]
plugins: HashMap<String, String>,
#[serde(default)]
task_config: TaskConfig,
#[serde(default)]
tasks: Tasks,
#[serde(default)]
settings: SettingsPartial,
}
#[derive(Debug, Default, Clone)]
pub struct MiseTomlToolList(Vec<MiseTomlTool>);
#[derive(Debug, Clone)]
pub struct MiseTomlTool {
pub tt: ToolVersionType,
pub options: ToolVersionOptions,
}
#[derive(Debug, Default, Clone)]
pub struct Tasks(pub BTreeMap<String, Task>);
#[derive(Debug, Default, Clone)]
pub struct EnvList(pub(crate) Vec<EnvDirective>);
impl MiseToml {
pub fn init(path: &Path) -> Self {
let mut context = BASE_CONTEXT.clone();
context.insert("config_root", path.parent().unwrap().to_str().unwrap());
Self {
path: path.to_path_buf(),
context,
..Default::default()
}
}
pub fn from_file(path: &Path) -> eyre::Result<Self> {
trace!("parsing: {}", display_path(path));
let body = file::read_to_string(path)?;
let mut rf: MiseToml = toml::from_str(&body)?;
rf.context = BASE_CONTEXT.clone();
rf.context
.insert("config_root", path.parent().unwrap().to_str().unwrap());
rf.path = path.to_path_buf();
for task in rf.tasks.0.values_mut() {
task.config_source.clone_from(&rf.path);
}
trace!("{}", rf.dump()?);
Ok(rf)
}
fn doc(&self) -> eyre::Result<&DocumentMut> {
self.doc.get_or_try_init(|| {
let body = file::read_to_string(&self.path).unwrap_or_default();
Ok(body.parse()?)
})
}
fn doc_mut(&mut self) -> eyre::Result<&mut DocumentMut> {
self.doc()?;
Ok(self.doc.get_mut().unwrap())
}
pub fn set_alias(&mut self, fa: &ForgeArg, from: &str, to: &str) -> eyre::Result<()> {
self.alias
.entry(fa.clone())
.or_default()
.insert(from.into(), to.into());
self.doc_mut()?
.entry("alias")
.or_insert_with(table)
.as_table_like_mut()
.unwrap()
.entry(&fa.to_string())
.or_insert_with(table)
.as_table_like_mut()
.unwrap()
.insert(from, value(to));
Ok(())
}
pub fn remove_alias(&mut self, fa: &ForgeArg, from: &str) -> eyre::Result<()> {
if let Some(aliases) = self
.doc_mut()?
.get_mut("alias")
.and_then(|v| v.as_table_mut())
{
if let Some(plugin_aliases) = aliases
.get_mut(&fa.to_string())
.and_then(|v| v.as_table_mut())
{
plugin_aliases.remove(from);
if plugin_aliases.is_empty() {
aliases.remove(&fa.to_string());
}
}
if aliases.is_empty() {
self.doc_mut()?.as_table_mut().remove("alias");
}
}
if let Some(aliases) = self.alias.get_mut(fa) {
aliases.remove(from);
if aliases.is_empty() {
self.alias.remove(fa);
}
}
Ok(())
}
pub fn update_env<V: Into<Value>>(&mut self, key: &str, value: V) -> eyre::Result<()> {
let env_tbl = self
.doc_mut()?
.entry("env")
.or_insert_with(table)
.as_table_like_mut()
.unwrap();
env_tbl.insert(key, toml_edit::value(value));
Ok(())
}
pub fn remove_env(&mut self, key: &str) -> eyre::Result<()> {
let env_tbl = self
.doc_mut()?
.entry("env")
.or_insert_with(table)
.as_table_like_mut()
.unwrap();
env_tbl.remove(key);
Ok(())
}
fn parse_template(&self, input: &str) -> eyre::Result<String> {
if !input.contains("{{") && !input.contains("{%") && !input.contains("{#") {
return Ok(input.to_string());
}
trust_check(&self.path)?;
let dir = self.path.parent();
let output = get_tera(dir)
.render_str(input, &self.context)
.wrap_err_with(|| {
let p = display_path(&self.path);
eyre!("failed to parse template {input} in {p}")
})?;
Ok(output)
}
}
impl ConfigFile for MiseToml {
fn get_path(&self) -> &Path {
self.path.as_path()
}
fn min_version(&self) -> &Option<Versioning> {
&self.min_version
}
fn project_root(&self) -> Option<&Path> {
let filename = self.path.file_name().unwrap_or_default().to_string_lossy();
match self.path.parent() {
Some(dir) => match dir {
dir if dir.starts_with(*dirs::CONFIG) => None,
dir if dir.starts_with(*dirs::SYSTEM) => None,
dir if dir == *dirs::HOME => None,
dir if !filename.starts_with('.') && dir.ends_with(".mise") => dir.parent(),
dir if !filename.starts_with('.') && dir.ends_with(".config/mise") => {
dir.parent().unwrap().parent()
}
dir => Some(dir),
},
None => None,
}
}
fn plugins(&self) -> HashMap<String, String> {
self.plugins.clone()
}
fn env_entries(&self) -> eyre::Result<Vec<EnvDirective>> {
let env_entries = self.env.0.iter().cloned();
let path_entries = self
.env_path
.iter()
.map(|p| EnvDirective::Path(p.clone()))
.collect_vec();
let env_files = self
.env_file
.iter()
.map(|p| EnvDirective::File(p.clone()))
.collect_vec();
let all = path_entries
.into_iter()
.chain(env_files)
.chain(env_entries)
.collect::<Vec<_>>();
if !all.is_empty() {
trust_check(&self.path)?;
}
Ok(all)
}
fn tasks(&self) -> Vec<&Task> {
self.tasks.0.values().collect()
}
fn remove_plugin(&mut self, fa: &ForgeArg) -> eyre::Result<()> {
self.tools.shift_remove(fa);
let doc = self.doc_mut()?;
if let Some(tools) = doc.get_mut("tools") {
if let Some(tools) = tools.as_table_like_mut() {
tools.remove(&fa.to_string());
if tools.is_empty() {
doc.as_table_mut().remove("tools");
}
}
}
Ok(())
}
fn replace_versions(&mut self, fa: &ForgeArg, versions: &[String]) -> eyre::Result<()> {
self.tools.entry(fa.clone()).or_default().0 = versions
.iter()
.map(|v| MiseTomlTool {
tt: ToolVersionType::Version(v.clone()),
options: Default::default(),
})
.collect();
let tools = self
.doc_mut()?
.entry("tools")
.or_insert_with(table)
.as_table_mut()
.unwrap();
if versions.len() == 1 {
tools.insert(&fa.to_string(), value(versions[0].clone()));
} else {
let mut arr = Array::new();
for v in versions {
arr.push(v);
}
tools.insert(&fa.to_string(), Item::Value(Value::Array(arr)));
}
Ok(())
}
fn save(&self) -> eyre::Result<()> {
let contents = self.dump()?;
if let Some(parent) = self.path.parent() {
create_dir_all(parent)?;
}
file::write(&self.path, contents)
}
fn dump(&self) -> eyre::Result<String> {
Ok(self.doc()?.to_string())
}
fn to_tool_request_set(&self) -> eyre::Result<ToolRequestSet> {
let source = ToolSource::MiseToml(self.path.clone());
let mut trs = ToolRequestSet::new();
for (fa, tvp) in &self.tools {
for tool in &tvp.0 {
if let ToolVersionType::Path(_) = &tool.tt {
trust_check(&self.path)?;
}
let version = self.parse_template(&tool.tt.to_string())?;
let mut options = tool.options.clone();
for v in options.values_mut() {
*v = self.parse_template(v)?;
}
let tvr = ToolVersionRequest::new_opts(fa.clone(), &version, options);
trs.add_version(tvr, &source);
}
}
Ok(trs)
}
fn aliases(&self) -> AliasMap {
self.alias.clone()
}
fn task_config(&self) -> &TaskConfig {
&self.task_config
}
}
impl Debug for MiseToml {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let tools = self.to_toolset().unwrap().to_string();
let title = format!("MiseToml({}): {tools}", &display_path(&self.path));
let mut d = f.debug_struct(&title);
if let Some(min_version) = &self.min_version {
d.field("min_version", &min_version.to_string());
}
if !self.env_file.is_empty() {
d.field("env_file", &self.env_file);
}
if let Ok(env) = self.env_entries() {
if !env.is_empty() {
d.field("env", &env);
}
}
if !self.alias.is_empty() {
d.field("alias", &self.alias);
}
if !self.plugins.is_empty() {
d.field("plugins", &self.plugins);
}
if self.task_config.includes.is_some() {
d.field("task_config", &self.task_config);
}
d.finish()
}
}
impl Clone for MiseToml {
fn clone(&self) -> Self {
Self {
min_version: self.min_version.clone(),
context: self.context.clone(),
path: self.path.clone(),
env_file: self.env_file.clone(),
env: self.env.clone(),
env_path: self.env_path.clone(),
alias: self.alias.clone(),
doc: self.doc.clone(),
tools: self.tools.clone(),
plugins: self.plugins.clone(),
tasks: self.tasks.clone(),
task_config: self.task_config.clone(),
settings: self.settings.clone(),
}
}
}
fn deserialize_version<'de, D>(deserializer: D) -> Result<Option<Versioning>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = serde::Deserialize::deserialize(deserializer)?;
match s {
Some(s) => Ok(Some(
Versioning::new(&s)
.ok_or(versions::Error::IllegalVersioning(s))
.map_err(serde::de::Error::custom)?,
)),
None => Ok(None),
}
}
impl<'de> de::Deserialize<'de> for EnvList {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct EnvManVisitor;
impl<'de> Visitor<'de> for EnvManVisitor {
type Value = EnvList;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("env table or array of env tables")
}
fn visit_seq<S>(self, mut seq: S) -> std::result::Result<Self::Value, S::Error>
where
S: de::SeqAccess<'de>,
{
let mut env = vec![];
while let Some(list) = seq.next_element::<EnvList>()? {
env.extend(list.0);
}
Ok(EnvList(env))
}
fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let mut env = vec![];
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"_" | "mise" => {
struct EnvDirectivePythonVenv {
path: PathBuf,
create: bool,
}
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct EnvDirectivePython {
#[serde(default)]
venv: Option<EnvDirectivePythonVenv>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct EnvDirectives {
#[serde(default, deserialize_with = "deserialize_arr")]
path: Vec<PathBuf>,
#[serde(default, deserialize_with = "deserialize_arr")]
file: Vec<PathBuf>,
#[serde(default, deserialize_with = "deserialize_arr")]
source: Vec<PathBuf>,
#[serde(default)]
python: EnvDirectivePython,
}
impl<'de> de::Deserialize<'de> for EnvDirectivePythonVenv {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct EnvDirectivePythonVenvVisitor;
impl<'de> Visitor<'de> for EnvDirectivePythonVenvVisitor {
type Value = EnvDirectivePythonVenv;
fn expecting(
&self,
formatter: &mut Formatter,
) -> std::fmt::Result
{
formatter.write_str("python venv directive")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(EnvDirectivePythonVenv {
path: v.into(),
create: false,
})
}
fn visit_map<M>(
self,
mut map: M,
) -> Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let mut path = None;
let mut create = false;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"path" => {
path = Some(map.next_value()?);
}
"create" => {
create = map.next_value()?;
}
_ => {
return Err(de::Error::unknown_field(
&key,
&["path", "create"],
));
}
}
}
let path = path
.ok_or_else(|| de::Error::missing_field("path"))?;
Ok(EnvDirectivePythonVenv { path, create })
}
}
const FIELDS: &[&str] = &["path", "create"];
deserializer.deserialize_struct(
"PythonVenv",
FIELDS,
EnvDirectivePythonVenvVisitor,
)
}
}
let directives = map.next_value::<EnvDirectives>()?;
for path in directives.path {
env.push(EnvDirective::Path(path));
}
for file in directives.file {
env.push(EnvDirective::File(file));
}
for source in directives.source {
env.push(EnvDirective::Source(source));
}
if let Some(venv) = directives.python.venv {
env.push(EnvDirective::PythonVenv {
path: venv.path,
create: venv.create,
});
}
}
_ => {
enum Val {
Int(i64),
Str(String),
Bool(bool),
}
impl<'de> de::Deserialize<'de> for Val {
fn deserialize<D>(
deserializer: D,
) -> std::result::Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct ValVisitor;
impl<'de> Visitor<'de> for ValVisitor {
type Value = Val;
fn expecting(
&self,
formatter: &mut Formatter,
) -> std::fmt::Result
{
formatter.write_str("env value")
}
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: de::Error,
{
match v {
true => Err(de::Error::custom(
"env values cannot be true",
)),
false => Ok(Val::Bool(v)),
}
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Val::Int(v))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Val::Str(v.to_string()))
}
}
deserializer.deserialize_any(ValVisitor)
}
}
let value = map.next_value::<Val>()?;
match value {
Val::Int(i) => {
env.push(EnvDirective::Val(key, i.to_string()));
}
Val::Str(s) => {
env.push(EnvDirective::Val(key, s));
}
Val::Bool(_b) => env.push(EnvDirective::Rm(key)),
}
}
}
}
Ok(EnvList(env))
}
}
deserializer.deserialize_any(EnvManVisitor)
}
}
impl<'de> de::Deserialize<'de> for MiseTomlToolList {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct MiseTomlToolListVisitor;
impl<'de> Visitor<'de> for MiseTomlToolListVisitor {
type Value = MiseTomlToolList;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("tool list")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let tt: ToolVersionType = v
.parse()
.map_err(|e| de::Error::custom(format!("invalid tool: {e}")))?;
Ok(MiseTomlToolList(vec![MiseTomlTool {
tt,
options: Default::default(),
}]))
}
fn visit_seq<S>(self, mut seq: S) -> std::result::Result<Self::Value, S::Error>
where
S: de::SeqAccess<'de>,
{
let mut tools = vec![];
while let Some(tool) = seq.next_element::<MiseTomlTool>()? {
tools.push(tool);
}
Ok(MiseTomlToolList(tools))
}
fn visit_map<M>(self, map: M) -> std::result::Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let mut options: BTreeMap<String, String> =
de::Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;
let tt: ToolVersionType = options
.remove("version")
.or_else(|| options.remove("path").map(|p| format!("path:{p}")))
.or_else(|| options.remove("prefix").map(|p| format!("prefix:{p}")))
.or_else(|| options.remove("ref").map(|p| format!("ref:{p}")))
.ok_or_else(|| de::Error::custom("missing version"))?
.parse()
.map_err(de::Error::custom)?;
Ok(MiseTomlToolList(vec![MiseTomlTool { tt, options }]))
}
}
deserializer.deserialize_any(MiseTomlToolListVisitor)
}
}
impl<'de> de::Deserialize<'de> for MiseTomlTool {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct MiseTomlToolVisitor;
impl<'de> Visitor<'de> for MiseTomlToolVisitor {
type Value = MiseTomlTool;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("tool definition")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let tt: ToolVersionType = v
.parse()
.map_err(|e| de::Error::custom(format!("invalid tool: {e}")))?;
Ok(MiseTomlTool {
tt,
options: Default::default(),
})
}
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let mut options: BTreeMap<String, String> =
de::Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;
let tt: ToolVersionType = options
.remove("version")
.or_else(|| options.remove("path").map(|p| format!("path:{p}")))
.or_else(|| options.remove("prefix").map(|p| format!("prefix:{p}")))
.or_else(|| options.remove("ref").map(|p| format!("ref:{p}")))
.ok_or_else(|| de::Error::custom("missing version"))?
.parse()
.map_err(de::Error::custom)?;
Ok(MiseTomlTool { tt, options })
}
}
deserializer.deserialize_any(MiseTomlToolVisitor)
}
}
impl<'de> de::Deserialize<'de> for Tasks {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct TasksVisitor;
impl<'de> Visitor<'de> for TasksVisitor {
type Value = Tasks;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("task, string, or array of strings")
}
fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
struct TaskDef(Task);
impl<'de> de::Deserialize<'de> for TaskDef {
fn deserialize<D>(deserializer: D) -> std::result::Result<TaskDef, D::Error>
where
D: de::Deserializer<'de>,
{
struct TaskDefVisitor;
impl<'de> Visitor<'de> for TaskDefVisitor {
type Value = TaskDef;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("task definition")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(TaskDef(Task {
run: vec![v.to_string()],
..Default::default()
}))
}
fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
where
S: de::SeqAccess<'de>,
{
let mut run = vec![];
while let Some(s) = seq.next_element::<String>()? {
run.push(s);
}
Ok(TaskDef(Task {
run,
..Default::default()
}))
}
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let t = de::Deserialize::deserialize(
de::value::MapAccessDeserializer::new(map),
)?;
Ok(TaskDef(t))
}
}
deserializer.deserialize_any(TaskDefVisitor)
}
}
let mut tasks = BTreeMap::new();
while let Some(name) = map.next_key::<String>()? {
let mut task = map.next_value::<TaskDef>()?.0;
task.name.clone_from(&name);
tasks.insert(name, task);
}
Ok(Tasks(tasks))
}
}
deserializer.deserialize_any(TasksVisitor)
}
}
impl<'de> de::Deserialize<'de> for ForgeArg {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
struct ForgeArgVisitor;
impl<'de> Visitor<'de> for ForgeArgVisitor {
type Value = ForgeArg;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("forge argument")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v.into())
}
}
deserializer.deserialize_any(ForgeArgVisitor)
}
}
fn deserialize_alias<'de, D>(deserializer: D) -> Result<AliasMap, D::Error>
where
D: Deserializer<'de>,
{
struct AliasMapVisitor;
impl<'de> Visitor<'de> for AliasMapVisitor {
type Value = AliasMap;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("alias table")
}
fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let mut aliases = AliasMap::new();
while let Some(plugin) = map.next_key::<String>()? {
let fa: ForgeArg = plugin.as_str().into();
let plugin_aliases = aliases.entry(fa).or_default();
for (from, to) in map.next_value::<BTreeMap<String, String>>()? {
plugin_aliases.insert(from, to);
}
}
Ok(aliases)
}
}
deserializer.deserialize_map(AliasMapVisitor)
}
#[cfg(test)]
mod tests {
use dirs::CWD;
use crate::test::replace_path;
use super::*;
#[test]
fn test_fixture() {
let cf = MiseToml::from_file(&dirs::HOME.join("fixtures/.mise.toml")).unwrap();
assert_debug_snapshot!(cf.env_entries().unwrap());
assert_debug_snapshot!(cf.plugins());
assert_snapshot!(replace_path(&format!("{:#?}", cf.to_toolset().unwrap())));
assert_debug_snapshot!(cf.alias);
assert_snapshot!(replace_path(&format!("{:#?}", &cf)));
}
#[test]
fn test_env() {
let p = CWD.as_ref().unwrap().join(".test.mise.toml");
file::write(
&p,
formatdoc! {r#"
min_version = "2024.1.1"
[env]
foo="bar"
"#},
)
.unwrap();
let cf = MiseToml::from_file(&p).unwrap();
let env = parse_env(file::read_to_string(&p).unwrap());
assert_debug_snapshot!(env, @r###""foo=bar""###);
let cf: Box<dyn ConfigFile> = Box::new(cf);
with_settings!({
assert_snapshot!(cf.dump().unwrap());
assert_snapshot!(cf);
assert_debug_snapshot!(cf);
});
}
#[test]
fn test_env_array_valid() {
let env = parse_env(formatdoc! {r#"
[[env]]
foo="bar"
[[env]]
bar="baz"
"#});
assert_snapshot!(env, @r###"
foo=bar
bar=baz
"###);
}
#[test]
fn test_path_dirs() {
let env = parse_env(formatdoc! {r#"
env_path=["/foo", "./bar"]
[env]
foo="bar"
"#});
assert_snapshot!(env, @r###"
path_add /foo
path_add ./bar
foo=bar
"###);
let env = parse_env(formatdoc! {r#"
env_path="./bar"
"#});
assert_snapshot!(env, @"path_add ./bar");
let env = parse_env(formatdoc! {r#"
[env]
_.path = "./bar"
"#});
assert_debug_snapshot!(env, @r###""path_add ./bar""###);
let env = parse_env(formatdoc! {r#"
[env]
_.path = ["/foo", "./bar"]
"#});
assert_snapshot!(env, @r###"
path_add /foo
path_add ./bar
"###);
let env = parse_env(formatdoc! {r#"
[[env]]
_.path = "/foo"
[[env]]
_.path = "./bar"
"#});
assert_snapshot!(env, @r###"
path_add /foo
path_add ./bar
"###);
let env = parse_env(formatdoc! {r#"
env_path = "/foo"
[env]
_.path = "./bar"
"#});
assert_snapshot!(env, @r###"
path_add /foo
path_add ./bar
"###);
}
#[test]
fn test_env_file() {
let env = parse_env(formatdoc! {r#"
env_file = ".env"
"#});
assert_debug_snapshot!(env, @r###""dotenv .env""###);
let env = parse_env(formatdoc! {r#"
env_file=[".env", ".env2"]
"#});
assert_debug_snapshot!(env, @r###""dotenv .env\ndotenv .env2""###);
let env = parse_env(formatdoc! {r#"
[env]
_.file = ".env"
"#});
assert_debug_snapshot!(env, @r###""dotenv .env""###);
let env = parse_env(formatdoc! {r#"
[env]
_.file = [".env", ".env2"]
"#});
assert_debug_snapshot!(env, @r###""dotenv .env\ndotenv .env2""###);
let env = parse_env(formatdoc! {r#"
dotenv = ".env"
[env]
_.file = ".env2"
"#});
assert_debug_snapshot!(env, @r###""dotenv .env\ndotenv .env2""###);
}
#[test]
fn test_set_alias() {
let p = CWD.as_ref().unwrap().join(".test.mise.toml");
file::write(
&p,
formatdoc! {r#"
[alias.node]
16 = "16.0.0"
18 = "18.0.0"
"#},
)
.unwrap();
let mut cf = MiseToml::from_file(&p).unwrap();
let node = "node".into();
let python = "python".into();
cf.set_alias(&node, "18", "18.0.1").unwrap();
cf.set_alias(&node, "20", "20.0.0").unwrap();
cf.set_alias(&python, "3.10", "3.10.0").unwrap();
assert_debug_snapshot!(cf.alias);
let cf: Box<dyn ConfigFile> = Box::new(cf);
assert_snapshot!(cf);
file::remove_file(&p).unwrap();
}
#[test]
fn test_remove_alias() {
let p = CWD.as_ref().unwrap().join(".test.mise.toml");
file::write(
&p,
formatdoc! {r#"
[alias.node]
16 = "16.0.0"
18 = "18.0.0"
[alias.python]
"3.10" = "3.10.0"
"#},
)
.unwrap();
let mut cf = MiseToml::from_file(&p).unwrap();
let node = "node".into();
let python = "python".into();
cf.remove_alias(&node, "16").unwrap();
cf.remove_alias(&python, "3.10").unwrap();
assert_debug_snapshot!(cf.alias);
let cf: Box<dyn ConfigFile> = Box::new(cf);
assert_snapshot!(cf.dump().unwrap());
assert_snapshot!(cf);
assert_debug_snapshot!(cf);
file::remove_file(&p).unwrap();
}
#[test]
fn test_replace_versions() {
let p = PathBuf::from("/tmp/.mise.toml");
file::write(
&p,
formatdoc! {r#"
[tools]
node = ["16.0.0", "18.0.0"]
"#},
)
.unwrap();
let mut cf = MiseToml::from_file(&p).unwrap();
let node = "node".into();
cf.replace_versions(&node, &["16.0.1".into(), "18.0.1".into()])
.unwrap();
assert_debug_snapshot!(cf.to_toolset().unwrap());
let cf: Box<dyn ConfigFile> = Box::new(cf);
assert_snapshot!(cf.dump().unwrap());
assert_snapshot!(cf);
assert_debug_snapshot!(cf);
}
#[test]
fn test_remove_plugin() {
let p = PathBuf::from("/tmp/.mise.toml");
file::write(
&p,
formatdoc! {r#"
[tools]
node = ["16.0.0", "18.0.0"]
"#},
)
.unwrap();
let mut cf = MiseToml::from_file(&p).unwrap();
cf.remove_plugin(&"node".into()).unwrap();
assert_debug_snapshot!(cf.to_toolset().unwrap());
let cf: Box<dyn ConfigFile> = Box::new(cf);
assert_snapshot!(cf.dump().unwrap());
assert_snapshot!(cf);
assert_debug_snapshot!(cf);
}
#[test]
fn test_fail_with_unknown_key() {
let _ = toml::from_str::<MiseToml>(&formatdoc! {r#"
invalid_key = true
"#})
.unwrap_err();
}
#[test]
fn test_env_entries() {
let toml = formatdoc! {r#"
[env]
foo1="1"
rm=false
_.path="/foo"
foo2="2"
_.file=".env"
foo3="3"
"#};
assert_snapshot!(parse_env(toml), @r###"
foo1=1
unset rm
path_add /foo
dotenv .env
foo2=2
foo3=3
"###);
}
#[test]
fn test_env_arr() {
let toml = formatdoc! {r#"
[[env]]
foo1="1"
rm=false
_.path="/foo"
foo2="2"
_.file=".env"
foo3="3"
_.source="/baz1"
[[env]]
foo4="4"
rm=false
_.file=".env2"
foo5="5"
_.path="/bar"
foo6="6"
_.source="/baz2"
"#};
assert_snapshot!(parse_env(toml), @r###"
foo1=1
unset rm
path_add /foo
dotenv .env
source /baz1
foo2=2
foo3=3
foo4=4
unset rm
path_add /bar
dotenv .env2
source /baz2
foo5=5
foo6=6
"###);
}
fn parse(s: String) -> MiseToml {
let p = CWD.as_ref().unwrap().join(".test.mise.toml");
file::write(&p, s).unwrap();
MiseToml::from_file(&p).unwrap()
}
fn parse_env(toml: String) -> String {
parse(toml).env_entries().unwrap().into_iter().join("\n")
}
}