use std::collections::HashSet;
use std::path::Path;
use theway_contract::attachments::AttachmentStore;
use theway_contract::user_input::{InputFilePart, InputPart};
use theway_transport::mentions::{mentions, truncate_mention};
use super::store_error;
pub(super) async fn parts(
store: &dyn AttachmentStore,
cwd: &Path,
text: &str,
) -> Result<Vec<InputPart>, String> {
let mut seen = HashSet::new();
let mut out = Vec::new();
for relative in mentions(text) {
if !seen.insert(relative.clone()) {
continue;
}
if let Some(part) = file_part(store, cwd, &relative).await? {
out.push(part);
}
}
Ok(out)
}
async fn file_part(
store: &dyn AttachmentStore,
cwd: &Path,
relative: &str,
) -> Result<Option<InputPart>, String> {
let Ok(text) = tokio::fs::read_to_string(cwd.join(relative)).await else {
return Ok(None);
};
let (body, truncated) = truncate_mention(&text);
let digest = store.put(body.as_bytes()).map_err(store_error)?;
Ok(Some(InputPart::File(InputFilePart {
path: relative.to_string(),
name: leaf_name(relative),
digest,
bytes: body.len() as u64,
media_type: media_type(relative).to_string(),
truncated,
})))
}
fn leaf_name(relative: &str) -> String {
match Path::new(relative).file_name() {
Some(name) if !name.is_empty() => name.to_string_lossy().into_owned(),
_ => relative.to_string(),
}
}
fn media_type(relative: &str) -> &'static str {
let Some(extension) = Path::new(relative).extension() else {
return "text/plain";
};
match extension.to_string_lossy().to_ascii_lowercase().as_str() {
"md" | "markdown" => "text/markdown",
"json" => "application/json",
"yaml" | "yml" => "application/yaml",
"html" | "htm" => "text/html",
"csv" => "text/csv",
"xml" => "text/xml",
_ => "text/plain",
}
}