use yaml_rust::{yaml, Yaml, YamlLoader};
use crate::{
constants, errors, eval, model,
model::{IndexMap, StringSet},
path, syntax,
};
pub fn parse(
app_context: &model::ApplicationContext,
string: &str,
config_verbose: u8,
config: &mut model::Configuration,
) -> Result<(), errors::GardenError> {
parse_recursive(app_context, string, config_verbose, config, None)
}
fn parse_recursive(
app_context: &model::ApplicationContext,
string: &str,
config_verbose: u8,
config: &mut model::Configuration,
current_include: Option<&std::path::Path>,
) -> Result<(), errors::GardenError> {
let docs =
YamlLoader::load_from_str(string).map_err(|scan_err| errors::GardenError::ReadConfig {
err: scan_err,
path: config.get_path_for_display(),
})?;
if docs.is_empty() {
return Err(errors::GardenError::EmptyConfiguration {
path: config.get_path()?.into(),
});
}
let doc = &docs[0];
if config_verbose > 2 {
dump_node(doc, 1, "");
}
if config.root.is_empty()
&& !config.root_is_dynamic
&& get_raw_str(
&doc[constants::GARDEN][constants::ROOT],
config.root.get_expr_mut(),
)
{
if config.root.is_empty() {
config.root_is_dynamic = true;
}
if config_verbose > 0 {
debug!("config: garden.root = {}", config.root.get_expr());
}
}
if get_str(&doc[constants::GARDEN][constants::SHELL], &mut config.shell) && config_verbose > 0 {
debug!("config: {} = {}", constants::GARDEN_SHELL, config.shell);
}
if get_str(
&doc[constants::GARDEN][constants::INTERACTIVE_SHELL],
&mut config.interactive_shell,
) && config_verbose > 0
{
debug!(
"config: {} = {}",
constants::GARDEN_INTERACTIVE_SHELL,
config.interactive_shell
);
}
if get_bool(
&doc[constants::GARDEN][constants::SHELL_ERREXIT],
&mut config.shell_exit_on_error,
) && config_verbose > 0
{
debug!(
"config: {} = {}",
constants::GARDEN_SHELL_ERREXIT,
config.shell_exit_on_error
);
}
if get_bool(
&doc[constants::GARDEN][constants::SHELL_WORDSPLIT],
&mut config.shell_word_split,
) && config_verbose > 0
{
debug!(
"config: {} = {}",
constants::GARDEN_SHELL_WORDSPLIT,
config.shell_word_split
);
}
if get_bool(
&doc[constants::GARDEN][constants::TREE_BRANCHES],
&mut config.tree_branches,
) && config_verbose > 0
{
debug!(
"config: {} = {}",
constants::GARDEN_TREE_BRANCHES,
config.tree_branches
);
}
if config_verbose > 1 {
debug!("config: built-in variables");
}
config.variables.insert(
string!(constants::GARDEN_ROOT),
model::Variable::from_expr(
constants::GARDEN_ROOT.to_string(),
config.root.get_expr().to_string(),
),
);
if let Some(config_path_raw) = config.dirname.as_ref() {
if let Ok(config_path) = path::canonicalize(config_path_raw) {
config.variables.insert(
string!(constants::GARDEN_CONFIG_DIR),
model::Variable::from_expr(
constants::GARDEN_CONFIG_DIR.to_string(),
config_path.to_string_lossy().to_string(),
),
);
}
}
config.update_quiet_and_verbose_variables(config.quiet, 0);
if !get_variables_map(&doc[constants::VARIABLES], &mut config.variables) && config_verbose > 1 {
debug!("config: no variables");
}
let mut config_includes = Vec::new();
if get_vec_variables(
constants::INCLUDES,
&doc[constants::GARDEN][constants::INCLUDES],
&mut config_includes,
) {
for garden_include in &config_includes {
let pathbuf = match config.eval_config_pathbuf_from_include(
app_context,
current_include,
garden_include.get_expr(),
) {
Some(pathbuf) => pathbuf,
None => continue,
};
if !pathbuf.exists() {
if config_verbose > 0 {
debug!(
"warning: garden.includes entry not found: {:?} -> {:?}",
garden_include, pathbuf
);
}
continue;
}
if pathbuf.exists() {
if let Ok(content) = std::fs::read_to_string(&pathbuf) {
parse_recursive(
app_context,
&content,
config_verbose,
config,
Some(&pathbuf),
)
.unwrap_or(());
}
}
}
if !get_variables_map(&doc[constants::VARIABLES], &mut config.variables)
&& config_verbose > 1
{
debug!("config: no reloaded variables");
}
}
if config_verbose > 1 {
debug!("config: grafts");
}
if !get_grafts(&doc[constants::GRAFTS], &mut config.grafts) && config_verbose > 1 {
debug!("config: no grafts");
}
get_multivariables(&doc[constants::ENVIRONMENT], &mut config.environment);
if config_verbose > 1 {
debug!("config: commands");
}
if !get_multivariables_map(&doc[constants::COMMANDS], &mut config.commands)
&& config_verbose > 1
{
debug!("config: no commands");
}
if config_verbose > 1 {
debug!("config: templates");
}
if !get_templates(
&doc["templates"],
&config.templates.clone(),
&mut config.templates,
) && config_verbose > 1
{
debug!("config: no templates");
}
if config_verbose > 1 {
debug!("config: trees");
}
if !get_trees(app_context, config, &doc[constants::TREES]) && config_verbose > 1 {
debug!("config: no trees");
}
if config_verbose > 1 {
debug!("config: groups");
}
if !get_groups(&doc[constants::GROUPS], &mut config.groups) && config_verbose > 1 {
debug!("config: no groups");
}
if config_verbose > 1 {
debug!("config: gardens");
}
if !get_gardens(&doc[constants::GARDENS], &mut config.gardens) && config_verbose > 1 {
debug!("config: no gardens");
}
Ok(())
}
fn print_indent(indent: usize) {
for _ in 0..indent {
print!(" ");
}
}
fn dump_node(yaml: &Yaml, indent: usize, prefix: &str) {
match yaml {
Yaml::String(value) => {
print_indent(indent);
println!("{prefix}\"{value}\"");
}
Yaml::Array(value) => {
for x in value {
dump_node(x, indent + 1, "- ");
}
}
Yaml::Hash(hash) => {
for (k, v) in hash {
print_indent(indent);
match k {
Yaml::String(x) => {
println!("{prefix}{x}:");
}
_ => {
println!("{prefix}{k:?}:");
}
}
dump_node(v, indent + 1, prefix);
}
}
_ => {
print_indent(indent);
println!("{yaml:?}");
}
}
}
fn get_raw_str(yaml: &Yaml, string: &mut String) -> bool {
match yaml {
Yaml::String(yaml_string) => {
string.clone_from(yaml_string);
true
}
_ => false,
}
}
fn get_str(yaml: &Yaml, string: &mut String) -> bool {
get_raw_str(yaml, string) && !string.is_empty()
}
fn get_str_trimmed(yaml: &Yaml, string: &mut String) -> bool {
match yaml {
Yaml::String(yaml_string) => {
*string = yaml_string.trim_end().to_string();
!string.is_empty()
}
_ => false,
}
}
fn get_i64(yaml: &Yaml, value: &mut i64) -> bool {
match yaml {
Yaml::Integer(yaml_integer) => {
*value = *yaml_integer;
true
}
_ => false,
}
}
fn get_bool(yaml: &Yaml, value: &mut bool) -> bool {
match yaml {
Yaml::Boolean(yaml_bool) => {
*value = *yaml_bool;
true
}
_ => false,
}
}
fn get_indexset_str(yaml: &Yaml, values: &mut StringSet) -> bool {
match yaml {
Yaml::String(yaml_string) => {
values.insert(yaml_string.clone());
true
}
Yaml::Array(yaml_vec) => {
for value in yaml_vec {
if let Yaml::String(value_str) = value {
values.insert(value_str.clone());
}
}
true
}
_ => false,
}
}
fn variable_from_yaml(name: String, yaml: &Yaml) -> Option<model::Variable> {
match yaml {
Yaml::String(yaml_str) => Some(model::Variable::from_expr(name, yaml_str.to_string())),
Yaml::Array(yaml_array) => {
yaml_array
.iter()
.next_back()
.and_then(|yaml_value| variable_from_yaml(name, yaml_value))
}
Yaml::Integer(yaml_int) => {
let int_value = yaml_int.to_string();
Some(model::Variable::from_resolved_expr(name, int_value))
}
Yaml::Boolean(yaml_bool) => {
let bool_value = syntax::bool_to_string(*yaml_bool);
Some(model::Variable::from_resolved_expr(name, bool_value))
}
Yaml::Hash(yaml_hash) => {
let expr = yaml_hash
.get(&Yaml::String(constants::VALUE.to_string()))
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let required = yaml_hash
.get(&Yaml::String(constants::REQUIRED.to_string()))
.and_then(|v| v.as_bool())
.unwrap_or(false);
Some(model::Variable::new(name, expr, required))
}
_ => {
None
}
}
}
fn get_variable(name: String, yaml: &Yaml, value: &mut model::Variable) -> bool {
if let Some(variable) = variable_from_yaml(name, yaml) {
*value = variable;
true
} else {
false
}
}
fn get_vec_variables(name: &str, yaml: &Yaml, vec: &mut Vec<model::Variable>) -> bool {
if let Yaml::Array(yaml_array) = yaml {
for value in yaml_array {
if let Some(variable) = variable_from_yaml(name.to_string(), value) {
vec.push(variable);
}
}
return true;
}
if let Some(variable) = variable_from_yaml(name.to_string(), yaml) {
vec.push(variable);
return true;
}
false
}
fn get_variables_map(yaml: &Yaml, map: &mut model::VariableMap) -> bool {
match yaml {
Yaml::Hash(hash) => {
for (k, v) in hash {
let key = match k.as_str() {
Some(key_value) => key_value.to_string(),
None => {
continue;
}
};
if let Some(variable) = variable_from_yaml(key.to_string(), v) {
map.insert(key, variable);
}
}
true
}
_ => false,
}
}
fn get_multivariables(yaml: &Yaml, vec: &mut Vec<model::MultiVariable>) -> bool {
if let Yaml::Hash(hash) = yaml {
for (k, v) in hash {
let key = match k.as_str() {
Some(key_value) => key_value.to_string(),
None => continue,
};
if let Yaml::Array(yaml_array) = v {
let mut variables = Vec::new();
for value in yaml_array {
if let Some(variable) = variable_from_yaml(key.to_string(), value) {
variables.push(variable);
}
}
vec.push(model::MultiVariable::new(key, variables));
continue;
}
if let Some(variable) = variable_from_yaml(key.to_string(), v) {
let variables = vec![variable];
vec.push(model::MultiVariable::new(key, variables));
}
}
return true;
}
false
}
fn get_multivariables_map(yaml: &Yaml, multivariables: &mut model::MultiVariableMap) -> bool {
match yaml {
Yaml::Hash(hash) => {
for (k, v) in hash {
let key = match k.as_str() {
Some(key_value) => key_value.to_string(),
None => continue,
};
if let Yaml::Array(yaml_array) = v {
let mut variables = Vec::new();
for value in yaml_array {
if let Some(variable) = variable_from_yaml(key.to_string(), value) {
variables.push(variable);
}
}
multivariables.insert(key, variables);
continue;
}
if let Some(variable) = variable_from_yaml(key.to_string(), v) {
let variables = vec![variable];
multivariables.insert(key, variables);
}
}
true
}
_ => false,
}
}
fn get_templates(
yaml: &Yaml,
config_templates: &IndexMap<String, model::Template>,
templates: &mut IndexMap<String, model::Template>,
) -> bool {
match yaml {
Yaml::Hash(hash) => {
for (name, value) in hash {
let template_name = match &name.as_str() {
Some(template_name) => template_name.to_string(),
None => continue,
};
templates.insert(
template_name,
get_template(name, value, config_templates, yaml),
);
}
true
}
_ => false,
}
}
fn get_template(
name: &Yaml,
value: &Yaml,
config_templates: &IndexMap<String, model::Template>,
templates: &Yaml,
) -> model::Template {
let mut template = model::Template::default();
get_str(name, template.get_name_mut());
{
let mut url = String::new();
if get_str(value, &mut url) {
template.tree.remotes.insert(
constants::ORIGIN.to_string(),
model::Variable::from_expr(constants::ORIGIN.to_string(), url),
);
return template;
}
if get_str(&value[constants::URL], &mut url) {
template.tree.remotes.insert(
string!(constants::ORIGIN),
model::Variable::from_expr(constants::URL.to_string(), url),
);
}
}
get_indexset_str(&value[constants::EXTEND], &mut template.extend);
for template_name in &template.extend {
if let Yaml::Hash(_) = templates[template_name.as_ref()] {
let base = get_template(
&Yaml::String(template_name.clone()),
&templates[template_name.as_ref()],
config_templates,
templates,
);
base.apply(&mut template.tree);
} else {
if let Some(base) = config_templates.get(template_name) {
base.apply(&mut template.tree);
}
}
template.tree.templates.truncate(0);
}
get_tree_fields(value, &mut template.tree);
template
}
fn get_trees(
app_context: &model::ApplicationContext,
config: &mut model::Configuration,
yaml: &Yaml,
) -> bool {
match yaml {
Yaml::Hash(hash) => {
for (name, value) in hash {
if let Yaml::String(url) = value {
let tree = get_tree_from_url(name, url);
if let Some(current_tree) = config.trees.get_mut(tree.get_name()) {
current_tree.clone_from_tree(&tree);
} else {
config.trees.insert(tree.get_name().to_string(), tree);
}
} else {
let tree = get_tree(app_context, config, name, value, hash, true);
let replace = match value[constants::REPLACE] {
Yaml::Boolean(value) => value,
_ => false,
};
let current_tree_opt = config.trees.get_mut(tree.get_name());
match current_tree_opt {
Some(current_tree) if !replace => {
current_tree.clone_from_tree(&tree);
}
_ => {
config.trees.insert(tree.get_name().to_string(), tree);
}
}
}
}
true
}
_ => false,
}
}
fn get_tree_from_url(name: &Yaml, url: &str) -> model::Tree {
let mut tree = model::Tree::default();
get_str(name, tree.get_name_mut());
let tree_name = tree.get_name().to_string();
tree.get_path_mut().set_expr(tree_name.to_string());
tree.get_path().set_value(tree_name);
tree.add_builtin_variables();
if syntax::is_git_dir(tree.get_path().get_expr()) {
tree.is_bare_repository = true;
}
tree.remotes.insert(
constants::ORIGIN.to_string(),
model::Variable::from_expr(constants::ORIGIN.to_string(), url.to_string()),
);
tree
}
#[inline]
fn get_tree_fields(value: &Yaml, tree: &mut model::Tree) {
get_variables_map(&value[constants::VARIABLES], &mut tree.variables);
get_multivariables_map(&value[constants::GITCONFIG], &mut tree.gitconfig);
get_str(&value[constants::DEFAULT_REMOTE], &mut tree.default_remote);
get_str_trimmed(&value[constants::DESCRIPTION], &mut tree.description);
get_str_variables_map(&value[constants::REMOTES], &mut tree.remotes);
get_vec_variables(constants::LINKS, &value[constants::LINKS], &mut tree.links);
get_multivariables(&value[constants::ENVIRONMENT], &mut tree.environment);
get_multivariables_map(&value[constants::COMMANDS], &mut tree.commands);
get_variable(
constants::BRANCH.to_string(),
&value[constants::BRANCH],
&mut tree.branch,
);
get_variables_map(&value[constants::BRANCHES], &mut tree.branches);
get_variable(
constants::SYMLINK.to_string(),
&value[constants::SYMLINK],
&mut tree.symlink,
);
get_variable(
constants::WORKTREE.to_string(),
&value[constants::WORKTREE],
&mut tree.worktree,
);
get_i64(&value[constants::DEPTH], &mut tree.clone_depth);
get_bool(&value[constants::BARE], &mut tree.is_bare_repository);
get_bool(&value[constants::SINGLE_BRANCH], &mut tree.is_single_branch);
{
let mut url = String::new();
if get_str(&value[constants::URL], &mut url) {
tree.remotes.insert(
tree.default_remote.to_string(),
model::Variable::from_expr(constants::URL.to_string(), url),
);
}
}
tree.update_flags();
}
fn get_tree(
app_context: &model::ApplicationContext,
config: &mut model::Configuration,
name: &Yaml,
value: &Yaml,
trees: &yaml::Hash,
variables: bool,
) -> model::Tree {
let mut tree = model::Tree::default();
let mut extend = String::new();
if get_str(&value[constants::EXTEND], &mut extend) {
let tree_name = Yaml::String(extend.clone());
if let Some(tree_values) = trees.get(&tree_name) {
let base_tree = get_tree(app_context, config, &tree_name, tree_values, trees, false);
tree.clone_from_tree(&base_tree);
} else {
if let Some(base) = config.get_tree(&extend) {
tree.clone_from_tree(base);
}
}
tree.templates.truncate(0); }
let mut parent_expr = String::new();
if get_str(&value[constants::WORKTREE], &mut parent_expr) {
let parent_name = eval::value(app_context, config, &parent_expr);
if !parent_expr.is_empty() {
let tree_name = Yaml::String(parent_name);
if let Some(tree_values) = trees.get(&tree_name) {
let base = get_tree(app_context, config, &tree_name, tree_values, trees, true);
tree.clone_from_tree(&base);
}
}
tree.templates.truncate(0); }
get_indexset_str(&value[constants::TEMPLATES], &mut tree.templates);
for template_name in &tree.templates.clone() {
if let Some(template) = config.templates.get(template_name) {
template.apply(&mut tree);
}
}
get_str(name, tree.get_name_mut());
if !get_str(&value[constants::PATH], tree.get_path_mut().get_expr_mut()) {
let tree_name = tree.get_name().to_string();
tree.get_path_mut().set_expr(tree_name.to_string());
tree.get_path().set_value(tree_name);
}
if syntax::is_git_dir(tree.get_path().get_expr()) {
tree.is_bare_repository = true;
}
if variables {
tree.add_builtin_variables();
}
get_tree_fields(value, &mut tree);
tree
}
fn get_str_variables_map(yaml: &Yaml, remotes: &mut model::VariableMap) {
let hash = match yaml {
Yaml::Hash(hash) => hash,
_ => return,
};
for (name, value) in hash {
if let (Some(name_str), Some(value_str)) = (name.as_str(), value.as_str()) {
remotes.insert(
name_str.to_string(),
model::Variable::from_expr(name_str.to_string(), value_str.to_string()),
);
}
}
}
fn get_groups(yaml: &Yaml, groups: &mut IndexMap<model::GroupName, model::Group>) -> bool {
match yaml {
Yaml::Hash(hash) => {
for (name, value) in hash {
let mut group = model::Group::default();
get_str(name, group.get_name_mut());
get_indexset_str(value, &mut group.members);
groups.insert(group.get_name_owned(), group);
}
true
}
_ => false,
}
}
fn get_gardens(yaml: &Yaml, gardens: &mut IndexMap<String, model::Garden>) -> bool {
match yaml {
Yaml::Hash(hash) => {
for (name, value) in hash {
let mut garden = model::Garden::default();
get_str(name, garden.get_name_mut());
get_indexset_str(&value[constants::GROUPS], &mut garden.groups);
get_indexset_str(&value[constants::TREES], &mut garden.trees);
get_multivariables_map(&value[constants::GITCONFIG], &mut garden.gitconfig);
get_variables_map(&value[constants::VARIABLES], &mut garden.variables);
get_multivariables(&value[constants::ENVIRONMENT], &mut garden.environment);
get_multivariables_map(&value[constants::COMMANDS], &mut garden.commands);
gardens.insert(garden.get_name().to_string(), garden);
}
true
}
_ => false,
}
}
fn get_grafts(yaml: &Yaml, grafts: &mut IndexMap<model::GardenName, model::Graft>) -> bool {
match yaml {
Yaml::Hash(yaml_hash) => {
for (name, value) in yaml_hash {
let graft = get_graft(name, value);
grafts.insert(graft.get_name().to_string(), graft);
}
true
}
_ => false,
}
}
fn get_graft(name: &Yaml, graft: &Yaml) -> model::Graft {
let mut graft_name = String::new();
let mut config = String::new();
let mut root = String::new();
get_str(name, &mut graft_name);
if !get_str(graft, &mut config) {
if let Yaml::Hash(_hash) = graft {
get_str(&graft[constants::CONFIG], &mut config);
get_str(&graft[constants::ROOT], &mut root);
}
}
model::Graft::new(graft_name, root, config)
}
pub fn read_yaml<P>(path: P) -> Result<Yaml, errors::GardenError>
where
P: std::convert::AsRef<std::path::Path> + std::fmt::Debug,
{
let string =
std::fs::read_to_string(&path).map_err(|io_err| errors::GardenError::ReadFile {
path: path.as_ref().into(),
err: io_err,
})?;
let docs =
YamlLoader::load_from_str(&string).map_err(|err| errors::GardenError::ReadConfig {
err,
path: path.as_ref().display().to_string(),
})?;
if docs.is_empty() {
return Err(errors::GardenError::EmptyConfiguration {
path: path.as_ref().into(),
});
}
Ok(docs[0].clone())
}
pub fn empty_doc() -> Yaml {
Yaml::Hash(yaml::Hash::new())
}
pub(crate) fn add_section(key: &str, doc: &mut Yaml) -> Result<(), errors::GardenError> {
let exists = doc[key].as_hash().is_some();
if !exists {
if let Yaml::Hash(doc_hash) = doc {
let key = Yaml::String(key.to_string());
doc_hash.insert(key, Yaml::Hash(yaml::Hash::new()));
} else {
return Err(errors::GardenError::InvalidConfiguration {
msg: "document is not a hash".into(),
});
}
}
Ok(())
}