From 15c19e24ee2ab014c91e86c93f524ebcefd818ba Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:09:18 +1000 Subject: [PATCH 1/5] fix(env): allow mixed map for env._.file --- e2e/env/test_env_file | 6 ++++-- schema/mise.json | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/e2e/env/test_env_file b/e2e/env/test_env_file index bafbdb3096..538794e789 100644 --- a/e2e/env/test_env_file +++ b/e2e/env/test_env_file @@ -17,12 +17,14 @@ assert "mise env" # does not error cat <mise.toml [env] -_.file = ['a', 'b.json'] +_.file = ['a', { path = 'b.json', tools = true }, { path = ['c.json'], redact = true }] EOF echo 'export A=1' >a echo '{"B": 2}' >b.json +echo '{"C": 3}' >c.json assert "mise env | grep -v PATH" "export A=1 -export B=2" +export B=2 +export C=3" cat <mise.toml [env] diff --git a/schema/mise.json b/schema/mise.json index 8524b81bc0..3d182a254c 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -84,11 +84,41 @@ }, { "description": "dotenv files to load", + "type": "array", "items": { - "description": "dotenv file to load", - "type": "string" - }, - "type": "array" + "oneOf": [ + { + "description": "dotenv file to load", + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "tools": { + "type": "boolean", + "description": "load tools before resolving" + }, + "redact": { + "type": "boolean", + "description": "redact the value from logs" + } + } + } + ] + } } ] }, From bc30b2ad210bdd3830dc33a1f82e331a76b8be74 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:14:14 +1000 Subject: [PATCH 2/5] test: add more tests --- e2e/env/test_env_file | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/e2e/env/test_env_file b/e2e/env/test_env_file index 538794e789..7c66d12c4a 100644 --- a/e2e/env/test_env_file +++ b/e2e/env/test_env_file @@ -17,14 +17,46 @@ assert "mise env" # does not error cat <mise.toml [env] -_.file = ['a', { path = 'b.json', tools = true }, { path = ['c.json'], redact = true }] +_.file = 'a' +EOF +echo 'export A=1' >a +assert "mise env | grep -v PATH" "export A=1" + +cat <mise.toml +[env] +_.file = { value = 'b.json' } +EOF +echo '{"B": 2}' >b.json +assert "mise env | grep -v PATH" "export B=2" + +cat <mise.toml +[env] +_.file = ['a', 'b.json'] +EOF +echo 'export A=1' >a +echo '{"B": 2}' >b.json +assert "mise env | grep -v PATH" "export A=1 +export B=2" + +cat <mise.toml +[env] +_.file = ['a', {path = 'b.json', tools = true}] +EOF +echo 'export A=1' >a +echo '{"B": 2}' >b.json +assert "mise env | grep -v PATH" "export A=1 +export B=2" + +cat <mise.toml +[[env]] +_.file = 'a' +[[env]] +_.file = [{ path = 'b.json', tools = true }] EOF echo 'export A=1' >a echo '{"B": 2}' >b.json -echo '{"C": 3}' >c.json assert "mise env | grep -v PATH" "export A=1 -export B=2 -export C=3" +export B=2" cat <mise.toml [env] From 030b472e1b349072345e1cd3dd8c15476964d4f5 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:19:02 +1000 Subject: [PATCH 3/5] fix(schema): update --- schema/mise-task.json | 265 +++++++++++++++++++++++++++++++++++--- schema/mise.json | 17 +-- xtasks/render/settings.ts | 1 + 3 files changed, 251 insertions(+), 32 deletions(-) diff --git a/schema/mise-task.json b/schema/mise-task.json index 47ef9b66ed..bca8abe5e7 100644 --- a/schema/mise-task.json +++ b/schema/mise-task.json @@ -148,22 +148,7 @@ "type": "string" }, "env": { - "additionalProperties": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "enum": [false], - "type": "boolean" - } - ] - }, - "description": "environment variables", - "type": "object" + "$ref": "#/$defs/env" }, "tools": { "description": "tools to install/activate before running this task", @@ -330,6 +315,254 @@ "type": "object" } ] + }, + "env": { + "additionalProperties": { + "oneOf": [ + { + "type": "object", + "properties": { + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "tools": { + "type": "boolean", + "description": "load tools before resolving" + }, + "redact": { + "type": "boolean", + "description": "redact the value from logs" + } + } + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "description": "environment variables", + "properties": { + "_": { + "description": "environment modules", + "additionalProperties": true, + "properties": { + "file": { + "oneOf": [ + { + "type": "object", + "properties": { + "path": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "tools": { + "type": "boolean", + "description": "load tools before resolving" + }, + "redact": { + "type": "boolean", + "description": "redact the value from logs" + } + } + }, + { + "description": "dotenv file to load", + "type": "string" + }, + { + "description": "dotenv files to load", + "type": "array", + "items": { + "oneOf": [ + { + "description": "dotenv file to load", + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "tools": { + "type": "boolean", + "description": "load tools before resolving" + }, + "redact": { + "type": "boolean", + "description": "redact the value from logs" + } + } + } + ] + } + } + ] + }, + "path": { + "oneOf": [ + { + "type": "object", + "properties": { + "path": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + }, + { + "description": "PATH entry to add", + "type": "string" + }, + { + "description": "PATH entries to add", + "items": { + "description": "PATH entry to add", + "type": "string" + }, + "type": "array" + } + ] + }, + "python": { + "description": "python environment", + "properties": { + "venv": { + "oneOf": [ + { + "description": "path to python virtual environment to use", + "type": "string" + }, + { + "description": "virtualenv options", + "properties": { + "create": { + "default": false, + "description": "create a new virtual environment if one does not exist", + "type": "boolean" + }, + "path": { + "description": "path to python virtual environment to use", + "type": "string" + }, + "python": { + "description": "python version to use", + "type": "string" + }, + "python_create_args": { + "description": "additional arguments to pass to python when creating a virtual environment", + "type": "array", + "items": { + "type": "string" + } + }, + "uv_create_args": { + "description": "additional arguments to pass to uv when creating a virtual environment", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["path"], + "type": "object" + } + ] + } + }, + "type": "object" + }, + "source": { + "oneOf": [ + { + "type": "object", + "properties": { + "path": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "tools": { + "type": "boolean", + "description": "load tools before resolving" + }, + "redact": { + "type": "boolean", + "description": "redact the value from logs" + } + } + }, + { + "description": "bash script to load", + "type": "string" + }, + { + "description": "bash scripts to load", + "items": { + "description": "bash script to load", + "type": "string" + }, + "type": "array" + } + ] + } + }, + "type": "object" + } + }, + "type": "object" } }, "description": "Config file for included mise tasks (https://mise.jdx.dev/tasks/#task-configuration)", diff --git a/schema/mise.json b/schema/mise.json index 3d182a254c..f192ff55f7 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -1163,22 +1163,7 @@ "type": "string" }, "env": { - "additionalProperties": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "enum": [false], - "type": "boolean" - } - ] - }, - "description": "environment variables", - "type": "object" + "$ref": "#/$defs/env" }, "tools": { "description": "tools to install/activate before running this task", diff --git a/xtasks/render/settings.ts b/xtasks/render/settings.ts index fe980836f0..47023179c6 100644 --- a/xtasks/render/settings.ts +++ b/xtasks/render/settings.ts @@ -124,6 +124,7 @@ const taskSchema = JSON.parse( fs.readFileSync("schema/mise-task.json", "utf-8"), ); taskSchema["$defs"].task = schema["$defs"].task; +taskSchema["$defs"].env = schema["$defs"].env; fs.writeFileSync( "schema/mise-task.json.tmp", JSON.stringify(taskSchema, null, 2), From e35680af720a76d1d90bc6f053ddb371abee963b Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:40:54 +1000 Subject: [PATCH 4/5] fix(toml): allow mixed array --- src/config/config_file/toml.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/config/config_file/toml.rs b/src/config/config_file/toml.rs index 53cba4989a..21de3e0db8 100644 --- a/src/config/config_file/toml.rs +++ b/src/config/config_file/toml.rs @@ -80,7 +80,7 @@ where { type Value = Vec; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("string or array of strings") + formatter.write_str("a string, a map, or a list of strings/maps") } fn visit_str(self, v: &str) -> std::result::Result @@ -91,17 +91,6 @@ where Ok(vec![v]) } - fn visit_seq(self, mut seq: S) -> std::result::Result - where - S: de::SeqAccess<'de>, - { - let mut v = vec![]; - while let Some(s) = seq.next_element::()? { - v.push(s.parse().map_err(de::Error::custom)?); - } - Ok(v) - } - fn visit_map(self, map: M) -> std::result::Result where M: de::MapAccess<'de>, @@ -110,6 +99,13 @@ where de::value::MapAccessDeserializer::new(map), )?]) } + + fn visit_seq(self, seq: S) -> std::result::Result + where + S: de::SeqAccess<'de>, + { + Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq)) + } } deserializer.deserialize_any(ArrVisitor(std::marker::PhantomData)) From e41a2d799a74bf3797bc886fa0212d0fd4090fb6 Mon Sep 17 00:00:00 2001 From: Risu <79110363+risu729@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:15:50 +1000 Subject: [PATCH 5/5] test: add test for deserializer --- src/config/config_file/toml.rs | 104 +++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/config/config_file/toml.rs b/src/config/config_file/toml.rs index 21de3e0db8..fd9b2068d3 100644 --- a/src/config/config_file/toml.rs +++ b/src/config/config_file/toml.rs @@ -114,6 +114,8 @@ where #[cfg(test)] mod tests { use super::*; + use serde::Deserialize; + use std::str::FromStr; #[test] fn test_parse_arr() { @@ -135,4 +137,106 @@ mod tests { assert_eq!(table.get("baz").unwrap().as_str().unwrap(), "qux"); assert_eq!(table.get("num").unwrap().as_integer().unwrap(), 123); } + + #[derive(Deserialize, Debug, PartialEq, Eq)] + #[serde(untagged)] + enum TestItem { + String(String), + Object { a: String, b: i64 }, + } + + impl FromStr for TestItem { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + Ok(TestItem::String(s.to_string())) + } + } + + #[derive(Deserialize, Debug, PartialEq)] + struct TestStruct { + #[serde(default, deserialize_with = "deserialize_arr")] + arr: Vec, + } + + #[test] + fn test_deserialize_arr_string() { + let toml_str = r#"arr = "hello""#; + let expected = TestStruct { + arr: vec![TestItem::String("hello".to_string())], + }; + let actual: TestStruct = toml::from_str(toml_str).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn test_deserialize_arr_string_list() { + let toml_str = r#"arr = ["hello", "world"]"#; + let expected = TestStruct { + arr: vec![ + TestItem::String("hello".to_string()), + TestItem::String("world".to_string()), + ], + }; + let actual: TestStruct = toml::from_str(toml_str).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn test_deserialize_arr_map() { + let toml_str = r#"arr = { a = "foo", b = 123 }"#; + let expected = TestStruct { + arr: vec![TestItem::Object { + a: "foo".to_string(), + b: 123, + }], + }; + let actual: TestStruct = toml::from_str(toml_str).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn test_deserialize_arr_map_list() { + let toml_str = r#" + arr = [ + { a = "foo", b = 123 }, + { a = "bar", b = 456 }, + ] + "#; + let expected = TestStruct { + arr: vec![ + TestItem::Object { + a: "foo".to_string(), + b: 123, + }, + TestItem::Object { + a: "bar".to_string(), + b: 456, + }, + ], + }; + let actual: TestStruct = toml::from_str(toml_str).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn test_deserialize_arr_mixed_list() { + let toml_str = r#" + arr = [ + "hello", + { a = "foo", b = 123 }, + ] + "#; + let expected = TestStruct { + arr: vec![ + TestItem::String("hello".to_string()), + TestItem::Object { + a: "foo".to_string(), + b: 123, + }, + ], + }; + let actual: TestStruct = toml::from_str(toml_str).unwrap(); + assert_eq!(actual, expected); + } }