Skip to content

Commit db9c848

Browse files
committed
new: Codegen improvements. (#1946)
* Update impl. * Add tests. * Improve locations. * Fixes. * Add tests. * Add object. * Add array. * Polish. * Test array args.
1 parent a191bb7 commit db9c848

24 files changed

Lines changed: 978 additions & 66 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,17 @@
2929
written files need to be removed.
3030
- Updated blob existence checks to be batched and parallelized.
3131
- Can now be enabled entirely through environment variables.
32+
- Updated and improved code generation:
33+
- Added support for `array` and `object` variable types in `template.yml`. The values must be JSON
34+
compatible.
35+
- Updated `generator.templates` to support `https://` URLs that point to an archive that can be
36+
unpacked.
3237
- Removed the restriction around `moon.{yml,pkl}` not being allowed as a task input. However, will
3338
not be included when using `**/*`.
3439

3540
#### 🐞 Fixes
3641

42+
- Fixed an issue where terminal prompt validation would not trigger.
3743
- Fixed an issue with remote cache hydration where multiple files with the same blob hash would fail
3844
to write them all.
3945
- Fixed some task/command argument quoting issues.

Cargo.lock

Lines changed: 31 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,10 @@ sha2 = "0.10.9"
6767
starbase = { version = "0.10.4" }
6868
starbase_archive = { version = "0.10.6", default-features = false, features = [
6969
"miette",
70-
"tar-gz",
70+
"tar-all",
71+
"zip-all",
7172
] }
72-
starbase_console = { version = "0.6.9", features = ["miette"] }
73+
starbase_console = { version = "0.6.10", features = ["miette"] }
7374
starbase_events = "0.7.2"
7475
starbase_sandbox = "0.9.3"
7576
starbase_shell = "0.7.5"
@@ -93,6 +94,7 @@ tokio = { version = "1.45.0", default-features = false, features = [
9394
tokio-util = "0.7.15"
9495
typescript_tsconfig_json = { version = "0.5.0", features = ["serialize"] }
9596
tracing = "0.1.41"
97+
url = "2.5.4"
9698
uuid = { version = "1.16.0", features = ["v4"] }
9799

98100
# proto/plugin related

crates/app/src/commands/generate.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use moon_console::{
1717
};
1818
use rustc_hash::FxHashMap;
1919
use starbase::AppResult;
20+
use starbase_utils::json::{self, JsonValue, serde_json};
2021
use std::path::PathBuf;
2122
use std::sync::Arc;
2223
use tera::Context as TemplateContext;
@@ -53,6 +54,28 @@ pub struct GenerateArgs {
5354
vars: Vec<String>,
5455
}
5556

57+
fn is_numeric(value: &str) -> bool {
58+
value
59+
.as_bytes()
60+
.iter()
61+
.all(|b| *b == b'-' || *b == b'.' || *b >= b'0' && *b <= b'9')
62+
}
63+
64+
fn parse_arg_into_json(value: &str) -> miette::Result<JsonValue> {
65+
if value == "true"
66+
|| value == "false"
67+
|| value == "null"
68+
|| value.starts_with('[')
69+
|| value.starts_with('{')
70+
|| value.starts_with('"')
71+
|| is_numeric(value)
72+
{
73+
Ok(json::parse(value)?)
74+
} else {
75+
Ok(JsonValue::String(value.into()))
76+
}
77+
}
78+
5679
#[instrument(skip(config))]
5780
pub fn parse_args_into_variables(
5881
args: &[String],
@@ -76,6 +99,14 @@ pub fn parse_args_into_variables(
7699
}
77100

78101
match cfg {
102+
TemplateVariable::Array(_) => {
103+
command = command.arg(
104+
Arg::new(name)
105+
.long(name)
106+
.action(ArgAction::Append)
107+
.value_parser(StringValueParser::new()),
108+
);
109+
}
79110
TemplateVariable::Boolean(_) => {
80111
command = command.arg(
81112
Arg::new(name)
@@ -114,6 +145,9 @@ pub fn parse_args_into_variables(
114145
.allow_negative_numbers(true),
115146
);
116147
}
148+
TemplateVariable::Object(_) => {
149+
debug!("Skipping object based arguments");
150+
}
117151
TemplateVariable::String(_) => {
118152
command = command.arg(
119153
Arg::new(name)
@@ -146,6 +180,25 @@ pub fn parse_args_into_variables(
146180
}
147181

148182
match cfg {
183+
TemplateVariable::Array(_) => {
184+
let mut list: Vec<JsonValue> = vec![];
185+
186+
if let Some(value) = matches.get_many::<String>(arg_name) {
187+
let value = value.collect::<Vec<_>>();
188+
189+
debug!(
190+
name,
191+
value = ?value,
192+
"Setting array variable"
193+
);
194+
195+
for item in value {
196+
list.push(parse_arg_into_json(item)?);
197+
}
198+
}
199+
200+
vars.insert(name, &list);
201+
}
149202
TemplateVariable::Boolean(_) => {
150203
// Booleans always have a value when matched, so only extract
151204
// the value when it was actually passed on the command line
@@ -183,6 +236,9 @@ pub fn parse_args_into_variables(
183236
vars.insert(name, value);
184237
}
185238
}
239+
TemplateVariable::Object(_) => {
240+
vars.insert(name, &FxHashMap::<String, JsonValue>::default());
241+
}
186242
TemplateVariable::String(_) => {
187243
if let Some(value) = matches.get_one::<String>(arg_name) {
188244
debug!(name, value, "Setting string variable");
@@ -246,6 +302,54 @@ pub async fn gather_variables(
246302
let required = config.is_required();
247303

248304
match config {
305+
TemplateVariable::Array(cfg) => {
306+
let value = if skip_prompts || cfg.prompt.is_none() {
307+
cfg.default.clone()
308+
} else {
309+
let mut value = String::new();
310+
311+
console
312+
.render_interactive(element! {
313+
Input(
314+
label: cfg.prompt.as_ref().unwrap(),
315+
description: Some("As a JSON string".into()),
316+
on_value: &mut value,
317+
validate: move |input: String| {
318+
let input = if input.is_empty() {
319+
if required {
320+
return Some("A value is required".into());
321+
} else {
322+
"[]"
323+
}
324+
} else {
325+
&input
326+
};
327+
328+
match serde_json::from_str::<JsonValue>(input) {
329+
Ok(data) => if data.is_array() {
330+
None
331+
} else {
332+
Some("Must be an array".into())
333+
},
334+
Err(error) => Some(format!("Invalid JSON: {error}")),
335+
}
336+
}
337+
)
338+
})
339+
.await?;
340+
341+
let data: JsonValue = json::parse(value)?;
342+
343+
match data {
344+
JsonValue::Array(inner) => inner,
345+
_ => vec![],
346+
}
347+
};
348+
349+
debug!(name, value = ?value, "Setting array variable");
350+
351+
context.insert(name, &value);
352+
}
249353
TemplateVariable::Boolean(cfg) => {
250354
let value = if skip_prompts || cfg.prompt.is_none() {
251355
cfg.default
@@ -302,6 +406,54 @@ pub async fn gather_variables(
302406

303407
context.insert(name, &value);
304408
}
409+
TemplateVariable::Object(cfg) => {
410+
let value = if skip_prompts || cfg.prompt.is_none() {
411+
cfg.default.clone()
412+
} else {
413+
let mut value = String::new();
414+
415+
console
416+
.render_interactive(element! {
417+
Input(
418+
label: cfg.prompt.as_ref().unwrap(),
419+
description: Some("As a JSON string".into()),
420+
on_value: &mut value,
421+
validate: move |input: String| {
422+
let input = if input.is_empty() {
423+
if required {
424+
return Some("A value is required".into());
425+
} else {
426+
"{}"
427+
}
428+
} else {
429+
&input
430+
};
431+
432+
match serde_json::from_str::<JsonValue>(input) {
433+
Ok(data) => if data.is_object() {
434+
None
435+
} else {
436+
Some("Must be an object".into())
437+
},
438+
Err(error) => Some(format!("Invalid JSON: {error}")),
439+
}
440+
}
441+
)
442+
})
443+
.await?;
444+
445+
let data: JsonValue = json::parse(value)?;
446+
447+
match data {
448+
JsonValue::Object(inner) => FxHashMap::from_iter(inner),
449+
_ => FxHashMap::default(),
450+
}
451+
};
452+
453+
debug!(name, value = ?value, "Setting object variable");
454+
455+
context.insert(name, &value);
456+
}
305457
TemplateVariable::String(cfg) => {
306458
let value = if skip_prompts || cfg.prompt.is_none() {
307459
cfg.default.clone()

crates/cli/tests/generate_test.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ use moon_app::commands::generate::parse_args_into_variables;
22
use moon_codegen::tera::{Number, Value};
33
use moon_common::path::standardize_separators;
44
use moon_config::{
5-
TemplateVariable, TemplateVariableBoolSetting, TemplateVariableEnumSetting,
6-
TemplateVariableEnumValue, TemplateVariableNumberSetting, TemplateVariableStringSetting,
5+
TemplateVariable, TemplateVariableArraySetting, TemplateVariableBoolSetting,
6+
TemplateVariableEnumSetting, TemplateVariableEnumValue, TemplateVariableNumberSetting,
7+
TemplateVariableStringSetting,
78
};
89
use moon_test_utils::{
910
Sandbox, assert_snapshot, create_sandbox_with_config, predicates::prelude::*,
@@ -467,6 +468,10 @@ mod cli_args {
467468
..Default::default()
468469
}),
469470
);
471+
vars.insert(
472+
"array".into(),
473+
TemplateVariable::Array(TemplateVariableArraySetting::default()),
474+
);
470475
vars.insert(
471476
"bool".into(),
472477
TemplateVariable::Boolean(TemplateVariableBoolSetting::default()),
@@ -513,6 +518,48 @@ mod cli_args {
513518
assert!(!context.contains_key("internal"));
514519
}
515520

521+
mod array {
522+
use super::*;
523+
524+
#[test]
525+
fn nothing_when_no_matching_arg() {
526+
let context = parse_args_into_variables(&[], &create_vars()).unwrap();
527+
528+
assert!(!context.contains_key("array"));
529+
}
530+
531+
#[test]
532+
fn sets_var() {
533+
let context = parse_args_into_variables(
534+
&[
535+
"--array".into(),
536+
"abc".into(),
537+
"--array".into(),
538+
"123".into(),
539+
"--array".into(),
540+
"456.78".into(),
541+
],
542+
&create_vars(),
543+
)
544+
.unwrap();
545+
546+
assert_eq!(
547+
context.get("array").unwrap(),
548+
&Value::Array(vec![
549+
Value::String("abc".into()),
550+
Value::Number(Number::from_i128(123).unwrap()),
551+
Value::Number(Number::from_f64(456.78).unwrap()),
552+
])
553+
);
554+
}
555+
556+
#[test]
557+
#[should_panic(expected = "a value is required")]
558+
fn errors_when_no_value() {
559+
parse_args_into_variables(&["--array".into()], &create_vars()).unwrap();
560+
}
561+
}
562+
516563
mod bool {
517564
use super::*;
518565

0 commit comments

Comments
 (0)