use super::ast::{ConfigAst, ConfigNode, DirectiveData, Separator};
use toride_ssh_core::Result;
const OPEN_PREFIX: &str = "# >>> toride ";
const CLOSE_PREFIX: &str = "# <<< toride ";
fn open_marker(name: &str) -> String {
format!("{OPEN_PREFIX}{name}")
}
fn close_marker(name: &str) -> String {
format!("{CLOSE_PREFIX}{name}")
}
#[derive(Debug, Clone)]
#[allow(dead_code, reason = "public API, not exercised in-workspace")]
pub struct ManagedBlock {
pub name: String,
pub nodes: Vec<ConfigNode>,
}
#[allow(dead_code, reason = "public API, not exercised in-workspace")]
pub fn extract_managed_block(ast: &ConfigAst, name: &str) -> Option<ManagedBlock> {
let open = open_marker(name);
let close = close_marker(name);
let mut inside = false;
let mut nodes = Vec::new();
for node in &ast.nodes {
match node {
ConfigNode::Comment { text, .. } if text.trim() == open => {
inside = true;
}
ConfigNode::Comment { text, .. } if text.trim() == close => {
return Some(ManagedBlock {
name: name.to_owned(),
nodes,
});
}
_ if inside => {
nodes.push(node.clone());
}
_ => {}
}
}
if inside {
Some(ManagedBlock {
name: name.to_owned(),
nodes,
})
} else {
None
}
}
pub fn list_managed_blocks(ast: &ConfigAst) -> Vec<String> {
let mut names = Vec::new();
for node in &ast.nodes {
if let ConfigNode::Comment { text, .. } = node {
let trimmed = text.trim();
if let Some(rest) = trimmed.strip_prefix(OPEN_PREFIX) {
names.push(rest.to_owned());
}
}
}
names
}
pub fn upsert_managed_block(ast: &mut ConfigAst, name: &str, directives: Vec<(String, String)>) {
let open = open_marker(name);
let close = close_marker(name);
let open_idx = ast
.nodes
.iter()
.position(|node| matches!(node, ConfigNode::Comment { text, .. } if text.trim() == open));
if let Some(start) = open_idx {
let end = ast.nodes[start + 1..]
.iter()
.position(
|node| matches!(node, ConfigNode::Comment { text, .. } if text.trim() == close),
)
.map(|i| start + 1 + i);
if let Some(end) = end {
ast.nodes.drain(start + 1..end);
let insert_at = start + 1;
ast.nodes.splice(
insert_at..insert_at,
directives.into_iter().map(|(key, value)| {
ConfigNode::Directive(Box::new(DirectiveData {
keyword: key,
separator: Separator::Space,
value,
comment: None,
indent: String::new(),
}))
}),
);
return;
}
ast.nodes.drain(start..);
}
let open_comment = ConfigNode::Comment {
text: open,
indent: String::new(),
};
let close_comment = ConfigNode::Comment {
text: close,
indent: String::new(),
};
if !ast.nodes.is_empty() {
ast.nodes.push(ConfigNode::BlankLine);
}
ast.nodes.push(open_comment);
ast.nodes.extend(directives.into_iter().map(|(key, value)| {
ConfigNode::Directive(Box::new(DirectiveData {
keyword: key,
separator: Separator::Space,
value,
comment: None,
indent: String::new(),
}))
}));
ast.nodes.push(close_comment);
}
pub fn remove_managed_block(ast: &mut ConfigAst, name: &str) -> Result<()> {
let open = open_marker(name);
let close = close_marker(name);
let open_idx = ast
.nodes
.iter()
.position(|node| matches!(node, ConfigNode::Comment { text, .. } if text.trim() == open));
let Some(start) = open_idx else {
return Err(toride_ssh_core::Error::ManagedBlockNotFound(format!(
"managed block {name} not found"
)));
};
let end = ast.nodes[start + 1..]
.iter()
.position(|node| matches!(node, ConfigNode::Comment { text, .. } if text.trim() == close))
.map(|i| start + 1 + i);
let Some(end) = end else {
return Err(toride_ssh_core::Error::ManagedBlockNotFound(format!(
"managed block {name} closing marker not found"
)));
};
ast.nodes.drain(start..=end);
if start > 0 && matches!(ast.nodes.get(start - 1), Some(ConfigNode::BlankLine)) {
ast.nodes.remove(start - 1);
}
Ok(())
}
#[allow(dead_code, reason = "public API, not exercised in-workspace")]
pub fn has_managed_block(ast: &ConfigAst, name: &str) -> bool {
let open = open_marker(name);
ast.nodes
.iter()
.any(|node| matches!(node, ConfigNode::Comment { text, .. } if text.trim() == open))
}
#[cfg(test)]
#[path = "managed.test.rs"]
mod tests;