Implement include optional=true (#3022)

* feat(niri): support `include optional=true "filename.kdl"`

* chore: warn if optional include ENOENT

* chore: validate include directive arguments and properties

Add proper validation to reject:
- Extra arguments beyond the path
- Unknown properties (other than "optional")
- Unexpected child nodes

* docs: implement suggested typographical/prose changes

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
This commit is contained in:
John Rinehart
2025-12-20 00:04:18 -05:00
committed by GitHub
parent c4462d0c7f
commit 7a237e519c
2 changed files with 79 additions and 5 deletions
+55 -5
View File
@@ -291,7 +291,51 @@ where
}
"include" => {
let path: PathBuf = utils::parse_arg_node("include", node, ctx)?;
// Parse the path argument
let mut iter_args = node.arguments.iter();
let path_val = iter_args.next().ok_or_else(|| {
DecodeError::missing(
node,
"additional argument for include path is required",
)
})?;
let path: PathBuf = knuffel::traits::DecodeScalar::decode(path_val, ctx)?;
// Check for extra arguments
if let Some(val) = iter_args.next() {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
"unexpected argument",
));
}
// Parse the optional property
let mut optional = false;
for (name, val) in &node.properties {
match &***name {
"optional" => {
optional = knuffel::traits::DecodeScalar::decode(val, ctx)?;
}
name_str => {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
format!("unexpected property `{}`", name_str.escape_default()),
));
}
}
}
// Check for unexpected children
for child in node.children() {
ctx.emit_error(DecodeError::unexpected(
child,
"node",
format!("unexpected node `{}`", child.node_name.escape_default()),
));
}
let base = ctx.get::<BasePath>().unwrap();
let path = base.0.join(path);
@@ -369,10 +413,16 @@ where
}
}
Err(err) => {
ctx.emit_error(DecodeError::missing(
node,
format!("failed to read included config from {path:?}: {err}"),
));
if optional && err.kind() == std::io::ErrorKind::NotFound {
// Warn about missing optional includes
warn!("optional include not found: {path:?}");
} else {
// Report all other errors normally
ctx.emit_error(DecodeError::missing(
node,
format!("failed to read included config from {path:?}: {err}"),
));
}
}
}
}