use std::path::Path;
use dbmd_core::edit::{self, EditError, SectionEdit};
use dbmd_core::parser::{extract_sections_in_file, Section, MAX_DBMD_FILE_BYTES};
use crate::cli::{SectionArgs, SectionCommand, SectionEditArgs, SectionGetArgs};
use crate::cmd::body::{path_str, read_content, EditTarget};
use crate::context::Context;
use crate::error::{CliError, CliResult, ExitCode};
use crate::sanitize::sanitize;
pub fn run(ctx: &Context, args: &SectionArgs) -> CliResult {
match &args.command {
SectionCommand::Get(get) => run_get(ctx, get),
SectionCommand::Set(edit) => run_edit(ctx, edit, Action::Set),
SectionCommand::Append(edit) => run_edit(ctx, edit, Action::Append),
}
}
#[derive(Clone, Copy)]
enum Action {
Set,
Append,
}
impl Action {
fn word(self) -> &'static str {
match self {
Action::Set => "set",
Action::Append => "append",
}
}
}
fn run_get(ctx: &Context, args: &SectionGetArgs) -> CliResult {
let path = Path::new(&args.file);
let text = dbmd_core::fsx::read_bounded_nofollow(path, MAX_DBMD_FILE_BYTES)
.and_then(|bytes| {
String::from_utf8(bytes)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
})
.map_err(|e| {
CliError::new(ExitCode::Runtime, "IO_ERROR", e.to_string())
.with_hint(format!("could not read `{}`", args.file))
})?;
let sections = extract_sections_in_file(&text);
let section = find_in(§ions, &args.heading, &args.file)?;
if ctx.json {
let obj = serde_json::json!({
"file": args.file,
"heading": section.heading,
"level": section.level,
"line": section.line,
"body": section.body,
});
let mut s = serde_json::to_string_pretty(&obj).unwrap_or_else(|_| "{}".to_string());
s.push('\n');
print!("{s}");
} else {
print!("{}", sanitize(§ion.body));
}
Ok(())
}
fn run_edit(ctx: &Context, args: &SectionEditArgs, action: Action) -> CliResult {
let content = read_content(args.text.as_deref(), args.body_file.as_deref())?;
let heading = args.heading.trim();
if heading.is_empty() {
return Err(CliError::new(
ExitCode::Runtime,
"BAD_HEADING",
"the heading text must not be empty",
));
}
let mut target = EditTarget::resolve(&args.file)?;
let attempted = match action {
Action::Set => edit::replace_section(&target.body, heading, &content),
Action::Append => edit::append_to_section(&target.body, heading, &content),
};
let (edited, created): (SectionEdit, bool) = match attempted {
Ok(edited) => (edited, false),
Err(EditError::SectionNotFound { .. }) if args.create => (
edit::append_section(&target.body, heading, args.level, &content),
true,
),
Err(e) => return Err(edit_error(e, &args.file)),
};
let index_updated = target.commit(&edited.body)?;
if ctx.json {
let out = serde_json::json!({
"file": path_str(&target.rel),
"action": action.word(),
"heading": heading,
"level": edited.level,
"created": created,
"index_updated": index_updated,
});
println!("{out}");
} else {
println!("{}", path_str(&target.rel));
if !index_updated {
eprintln!(
" warning: index not updated; run `dbmd index rebuild --folder <type-folder>`"
);
}
}
Ok(())
}
fn find_in<'a>(
sections: &'a [Section],
heading: &str,
file: &str,
) -> Result<&'a Section, CliError> {
let want = heading.trim();
let matches: Vec<&Section> = sections.iter().filter(|s| s.heading == want).collect();
match matches.as_slice() {
[] => Err(section_not_found(want, file)),
[one] => Ok(one),
many => Err(section_ambiguous(
want,
many.iter().map(|s| s.line).collect(),
)),
}
}
fn edit_error(error: EditError, file: &str) -> CliError {
match error {
EditError::SectionNotFound { heading } => {
section_not_found(&heading, file).with_hint(format!(
"run `dbmd sections {file}` to list headings, or pass `--create` to add the section"
))
}
EditError::SectionAmbiguous { heading, lines } => section_ambiguous(&heading, lines),
}
}
fn section_not_found(heading: &str, file: &str) -> CliError {
CliError::new(
ExitCode::Runtime,
"SECTION_NOT_FOUND",
format!("no section with heading `{heading}` in {file}"),
)
.with_hint(format!("run `dbmd sections {file}` to list headings"))
}
fn section_ambiguous(heading: &str, lines: Vec<u32>) -> CliError {
CliError::new(
ExitCode::Runtime,
"SECTION_AMBIGUOUS",
format!(
"heading `{heading}` matches {} sections; make headings unique to address them",
lines.len()
),
)
.with_details(serde_json::json!({ "lines": lines }))
}