Skip to content

Commit 1baa581

Browse files
committed
garden: add support for required variables
Add a "required variables" concept to garden. Variables can now be defined using a YAML hash syntax with a "required" key that will make garden abort its execution when the variable resolves to an empty value. Closes #21 Ref: https://gitlab.com/garden-rs/garden/-/issues/21
1 parent d93b523 commit 1baa581

File tree

10 files changed

+290
-68
lines changed

10 files changed

+290
-68
lines changed

doc/src/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
**Features**:
66

7+
- Garden variables can now be defined as being *required* variables. A required variable
8+
must evaluate to a non-empty string. If a required variable evaluates to an empty value
9+
then an error is reported and execution is aborted.
10+
([#21](https://gitlab.com/garden-rs/garden/-/issues/21))
11+
12+
713
- `garden ls -cc` now displays command recipes.
814

915
**Development**:

doc/src/configuration.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,45 @@ variables defined at the global scope. Variables defined in garden scope
256256
override/replace variables defined in a tree scope.
257257

258258

259+
### Required Variables
260+
261+
Required variables prevent empty values from being used at runtime.
262+
A variable can be defined as required using the YAML syntax below.
263+
264+
```yaml
265+
variables:
266+
# This is a required variable.
267+
required-value:
268+
required: true
269+
value: ${default-value}
270+
# The ${default-value} expression above might resolve to an empty value.
271+
# Required variables prevent empty values from being used at runtime.
272+
273+
default-value: $ test -f garden.yaml && echo not empty
274+
# This a normal variable using an exec expression that might result in an empty value.
275+
276+
# Commands that use empty required variables are prevented from running.
277+
commands:
278+
example: echo required-value = ${required-value}
279+
```
280+
281+
A required variable is can be used just like a regular variable. The only difference is
282+
that it terminates `garden` when it evaluates to an empty value in the course of
283+
evaluating a garden command or expression.
284+
285+
The `required-value` variable above is a required variable. When `default-value`
286+
evaluates to an empty value then an error will be reported and execution will be stopped
287+
right before the command that references the variable is run.
288+
289+
Running `garden example` with the example configuration above will not execute the
290+
`echo` inside of the `example` garden command. An error will be reported before the
291+
command is run and `garden` will exit with exit code 65
292+
(`EX_DATAERR` from `/usr/include/sysexits.h`).
293+
294+
You can specify values for (required) variables using the `--define | -D name=value`
295+
command-line arguments.
296+
297+
259298
## Built-in variables
260299

261300
Garden automatically defines some built-in variables that can be useful
@@ -275,6 +314,7 @@ that re-execute `garden`. These variables allow you to forward the user-specifie
275314
* **GARDEN_CMD_QUIET** -- `--quiet` when `--quiet` is specified, empty otherwise.
276315
* **GARDEN_CMD_VERBOSE** -- `-v`, `-vv` etc. when verbosity is increased, empty otherwise.
277316

317+
278318
## Environment Variables
279319

280320
The "environment" block defines variables that are stored in the environment.

src/cmds/init.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ pub fn main(options: &cli::MainOptions, init_options: &mut InitOptions) -> Resul
9292
};
9393

9494
let mut config = model::Configuration::new();
95-
config.root = model::Variable::from_expr(init_options.root.clone());
95+
config.root =
96+
model::Variable::from_expr(constants::ROOT.to_string(), init_options.root.clone());
9697
config.root_path.clone_from(&dirname);
9798
config.path = Some(config_path.clone());
9899

src/config/reader.rs

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,21 @@ fn parse_recursive(
125125
// Provide GARDEN_ROOT.
126126
config.variables.insert(
127127
string!(constants::GARDEN_ROOT),
128-
model::Variable::from_expr(config.root.get_expr().to_string()),
128+
model::Variable::from_expr(
129+
constants::GARDEN_ROOT.to_string(),
130+
config.root.get_expr().to_string(),
131+
),
129132
);
130133

131134
if let Some(config_path_raw) = config.dirname.as_ref() {
132135
// Calculate an absolute path for GARDEN_CONFIG_DIR.
133136
if let Ok(config_path) = path::canonicalize(config_path_raw) {
134137
config.variables.insert(
135138
string!(constants::GARDEN_CONFIG_DIR),
136-
model::Variable::from_expr(config_path.to_string_lossy().to_string()),
139+
model::Variable::from_expr(
140+
constants::GARDEN_CONFIG_DIR.to_string(),
141+
config_path.to_string_lossy().to_string(),
142+
),
137143
);
138144
}
139145
}
@@ -156,6 +162,7 @@ fn parse_recursive(
156162
// override the same variables when also defined in an included garden file.
157163
let mut config_includes = Vec::new();
158164
if get_vec_variables(
165+
constants::INCLUDES,
159166
&doc[constants::GARDEN][constants::INCLUDES],
160167
&mut config_includes,
161168
) {
@@ -375,13 +382,13 @@ fn get_indexset_str(yaml: &Yaml, values: &mut StringSet) -> bool {
375382
}
376383

377384
/// Construct a model::Variable from a ran YAML object.
378-
fn variable_from_yaml(yaml: &Yaml) -> Option<model::Variable> {
385+
fn variable_from_yaml(name: String, yaml: &Yaml) -> Option<model::Variable> {
379386
match yaml {
380-
Yaml::String(yaml_str) => Some(model::Variable::from_expr(yaml_str.to_string())),
387+
Yaml::String(yaml_str) => Some(model::Variable::from_expr(name, yaml_str.to_string())),
381388
Yaml::Array(yaml_array) => {
382389
// If we see an array we loop over so that the first value wins.
383-
for array_value in yaml_array.iter().rev() {
384-
return variable_from_yaml(array_value);
390+
if let Some(array_value) = yaml_array.iter().next_back() {
391+
return variable_from_yaml(name, array_value);
385392
}
386393

387394
None
@@ -390,13 +397,30 @@ fn variable_from_yaml(yaml: &Yaml) -> Option<model::Variable> {
390397
// Integers are already resolved.
391398
let int_value = yaml_int.to_string();
392399

393-
Some(model::Variable::from_resolved_expr(int_value))
400+
Some(model::Variable::from_resolved_expr(name, int_value))
394401
}
395402
Yaml::Boolean(yaml_bool) => {
396403
// Booleans are already resolved.
397404
let bool_value = syntax::bool_to_string(*yaml_bool);
398405

399-
Some(model::Variable::from_resolved_expr(bool_value))
406+
Some(model::Variable::from_resolved_expr(name, bool_value))
407+
}
408+
Yaml::Hash(yaml_hash) => {
409+
let required = yaml_hash
410+
.get(&Yaml::String(constants::REQUIRED.to_string()))
411+
.and_then(|v| v.as_bool())
412+
.unwrap_or(false);
413+
let value = yaml_hash
414+
.get(&Yaml::String(constants::VALUE.to_string()))
415+
.and_then(|v| v.as_str())
416+
.unwrap_or_default()
417+
.to_string();
418+
419+
if required {
420+
Some(model::Variable::from_required_expr(name, value))
421+
} else {
422+
Some(model::Variable::from_expr(name, value))
423+
}
400424
}
401425
_ => {
402426
// dump_node(yaml, 1, "");
@@ -406,8 +430,8 @@ fn variable_from_yaml(yaml: &Yaml) -> Option<model::Variable> {
406430
}
407431

408432
// Extract a `Variable` from `yaml`. Return `false` when `yaml` is not a `Yaml::String`.
409-
fn get_variable(yaml: &Yaml, value: &mut model::Variable) -> bool {
410-
if let Some(variable) = variable_from_yaml(yaml) {
433+
fn get_variable(name: String, yaml: &Yaml, value: &mut model::Variable) -> bool {
434+
if let Some(variable) = variable_from_yaml(name, yaml) {
411435
*value = variable;
412436

413437
true
@@ -417,17 +441,17 @@ fn get_variable(yaml: &Yaml, value: &mut model::Variable) -> bool {
417441
}
418442

419443
/// Promote `Yaml::String` or `Yaml::Array<Yaml::String>` into a `Vec<Variable>`.
420-
fn get_vec_variables(yaml: &Yaml, vec: &mut Vec<model::Variable>) -> bool {
444+
fn get_vec_variables(name: &str, yaml: &Yaml, vec: &mut Vec<model::Variable>) -> bool {
421445
if let Yaml::Array(yaml_array) = yaml {
422446
for value in yaml_array {
423-
if let Some(variable) = variable_from_yaml(value) {
447+
if let Some(variable) = variable_from_yaml(name.to_string(), value) {
424448
vec.push(variable);
425449
}
426450
}
427451
return true;
428452
}
429453

430-
if let Some(variable) = variable_from_yaml(yaml) {
454+
if let Some(variable) = variable_from_yaml(name.to_string(), yaml) {
431455
vec.push(variable);
432456
return true;
433457
}
@@ -447,7 +471,7 @@ fn get_variables_map(yaml: &Yaml, map: &mut model::VariableMap) -> bool {
447471
continue;
448472
}
449473
};
450-
if let Some(variable) = variable_from_yaml(v) {
474+
if let Some(variable) = variable_from_yaml(key.to_string(), v) {
451475
map.insert(key, variable);
452476
}
453477
}
@@ -469,15 +493,15 @@ fn get_multivariables(yaml: &Yaml, vec: &mut Vec<model::MultiVariable>) -> bool
469493
if let Yaml::Array(yaml_array) = v {
470494
let mut variables = Vec::new();
471495
for value in yaml_array {
472-
if let Some(variable) = variable_from_yaml(value) {
496+
if let Some(variable) = variable_from_yaml(key.to_string(), value) {
473497
variables.push(variable);
474498
}
475499
}
476500
vec.push(model::MultiVariable::new(key, variables));
477501
continue;
478502
}
479503

480-
if let Some(variable) = variable_from_yaml(v) {
504+
if let Some(variable) = variable_from_yaml(key.to_string(), v) {
481505
let variables = vec![variable];
482506
vec.push(model::MultiVariable::new(key, variables));
483507
}
@@ -501,15 +525,15 @@ fn get_multivariables_map(yaml: &Yaml, multivariables: &mut model::MultiVariable
501525
if let Yaml::Array(yaml_array) = v {
502526
let mut variables = Vec::new();
503527
for value in yaml_array {
504-
if let Some(variable) = variable_from_yaml(value) {
528+
if let Some(variable) = variable_from_yaml(key.to_string(), value) {
505529
variables.push(variable);
506530
}
507531
}
508532
multivariables.insert(key, variables);
509533
continue;
510534
}
511535

512-
if let Some(variable) = variable_from_yaml(v) {
536+
if let Some(variable) = variable_from_yaml(key.to_string(), v) {
513537
let variables = vec![variable];
514538
multivariables.insert(key, variables);
515539
}
@@ -561,19 +585,19 @@ fn get_template(
561585
// templates:
562586
// example: git://git.example.org/example/repo.git
563587
if get_str(value, &mut url) {
564-
template
565-
.tree
566-
.remotes
567-
.insert(string!(constants::ORIGIN), model::Variable::from_expr(url));
588+
template.tree.remotes.insert(
589+
constants::ORIGIN.to_string(),
590+
model::Variable::from_expr(constants::ORIGIN.to_string(), url),
591+
);
568592
return template;
569593
}
570594
// If a `<url>` is configured then populate the "origin" remote.
571595
// The first remote is "origin" by convention.
572596
if get_str(&value[constants::URL], &mut url) {
573-
template
574-
.tree
575-
.remotes
576-
.insert(string!(constants::ORIGIN), model::Variable::from_expr(url));
597+
template.tree.remotes.insert(
598+
string!(constants::ORIGIN),
599+
model::Variable::from_expr(constants::URL.to_string(), url),
600+
);
577601
}
578602
}
579603

@@ -671,8 +695,8 @@ fn get_tree_from_url(name: &Yaml, url: &str) -> model::Tree {
671695
tree.is_bare_repository = true;
672696
}
673697
tree.remotes.insert(
674-
string!(constants::ORIGIN),
675-
model::Variable::from_expr(url.to_string()),
698+
constants::ORIGIN.to_string(),
699+
model::Variable::from_expr(constants::ORIGIN.to_string(), url.to_string()),
676700
);
677701

678702
tree
@@ -686,15 +710,27 @@ fn get_tree_fields(value: &Yaml, tree: &mut model::Tree) {
686710
get_str(&value[constants::DEFAULT_REMOTE], &mut tree.default_remote);
687711
get_str_trimmed(&value[constants::DESCRIPTION], &mut tree.description);
688712
get_str_variables_map(&value[constants::REMOTES], &mut tree.remotes);
689-
get_vec_variables(&value[constants::LINKS], &mut tree.links);
713+
get_vec_variables(constants::LINKS, &value[constants::LINKS], &mut tree.links);
690714

691715
get_multivariables(&value[constants::ENVIRONMENT], &mut tree.environment);
692716
get_multivariables_map(&value[constants::COMMANDS], &mut tree.commands);
693717

694-
get_variable(&value[constants::BRANCH], &mut tree.branch);
718+
get_variable(
719+
constants::BRANCH.to_string(),
720+
&value[constants::BRANCH],
721+
&mut tree.branch,
722+
);
695723
get_variables_map(&value[constants::BRANCHES], &mut tree.branches);
696-
get_variable(&value[constants::SYMLINK], &mut tree.symlink);
697-
get_variable(&value[constants::WORKTREE], &mut tree.worktree);
724+
get_variable(
725+
constants::SYMLINK.to_string(),
726+
&value[constants::SYMLINK],
727+
&mut tree.symlink,
728+
);
729+
get_variable(
730+
constants::WORKTREE.to_string(),
731+
&value[constants::WORKTREE],
732+
&mut tree.worktree,
733+
);
698734

699735
get_i64(&value[constants::DEPTH], &mut tree.clone_depth);
700736
get_bool(&value[constants::BARE], &mut tree.is_bare_repository);
@@ -706,7 +742,7 @@ fn get_tree_fields(value: &Yaml, tree: &mut model::Tree) {
706742
if get_str(&value[constants::URL], &mut url) {
707743
tree.remotes.insert(
708744
tree.default_remote.to_string(),
709-
model::Variable::from_expr(url),
745+
model::Variable::from_expr(constants::URL.to_string(), url),
710746
);
711747
}
712748
}
@@ -803,7 +839,7 @@ fn get_str_variables_map(yaml: &Yaml, remotes: &mut model::VariableMap) {
803839
if let (Some(name_str), Some(value_str)) = (name.as_str(), value.as_str()) {
804840
remotes.insert(
805841
name_str.to_string(),
806-
model::Variable::from_expr(value_str.to_string()),
842+
model::Variable::from_expr(name_str.to_string(), value_str.to_string()),
807843
);
808844
}
809845
}

src/constants.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ pub const REMOTES: &str = "remotes";
130130
/// encountered.
131131
pub const REPLACE: &str = "replace";
132132

133+
/// The "required" key in a variable hash definition causes the variable to require
134+
/// that it evaluate to a non-empty value.
135+
pub const REQUIRED: &str = "required";
136+
133137
/// The "root" key in the garden block defines where trees are located and grown.
134138
pub const ROOT: &str = "root";
135139

@@ -208,6 +212,9 @@ pub const URL: &str = "url";
208212
/// can use "$ exec" expressions to capture stdout from a command.
209213
pub const VARIABLES: &str = "variables";
210214

215+
/// The "value" field in a Variable dict represents the variable's expression value.
216+
pub const VALUE: &str = "value";
217+
211218
/// The "worktree" key in a tree block is used to refer to a parent
212219
/// tree that will be used to grow the tree using "git worktree add".
213220
pub const WORKTREE: &str = "worktree";

0 commit comments

Comments
 (0)