use reqwest::blocking::Client;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use crate::utils::progress;
#[derive(Debug)]
pub enum ManifestError {
HttpError(String),
ParseError(String),
NotFound(String),
InvalidPath(String),
}
impl fmt::Display for ManifestError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ManifestError::HttpError(msg) => write!(f, "HTTP Error: {}", msg),
ManifestError::ParseError(msg) => write!(f, "Parse Error: {}", msg),
ManifestError::NotFound(msg) => write!(f, "Not Found: {}", msg),
ManifestError::InvalidPath(msg) => write!(f, "Invalid Path: {}", msg),
}
}
}
impl Error for ManifestError {}
pub struct ManifestNavigator {
url: String,
base_url: String,
client: Client,
}
#[derive(Debug, Clone)]
pub struct FileEntry {
pub name: String,
pub is_directory: bool,
pub full_url: String,
}
impl ManifestNavigator {
pub fn new(url: &str) -> Result<Self, ManifestError> {
let url = url.trim_end_matches('/');
let base_url = if url.ends_with("/manifest.yml") {
url.trim_end_matches("/manifest.yml").to_string()
} else if url.ends_with("manifest.yml") {
url.trim_end_matches("manifest.yml")
.trim_end_matches('/')
.to_string()
} else {
return Err(ManifestError::InvalidPath(
"URL must point to a manifest.yml file".to_string(),
));
};
Ok(Self {
url: url.to_string(),
base_url,
client: Client::new(),
})
}
pub fn fetch_manifest(&self) -> Result<HashMap<String, String>, ManifestError> {
let pb = progress::spinner("Fetching manifest...");
let response = self
.client
.get(&self.url)
.send()
.map_err(|e| ManifestError::HttpError(e.to_string()))?;
if !response.status().is_success() {
pb.finish_and_clear();
return Err(ManifestError::NotFound(format!(
"Manifest not found at: {}",
self.url
)));
}
pb.set_message("Manifest fetched successfully");
pb.finish_and_clear();
let content = response
.text()
.map_err(|e| ManifestError::HttpError(e.to_string()))?;
self.parse_manifest(&content)
}
fn parse_manifest(&self, content: &str) -> Result<HashMap<String, String>, ManifestError> {
let mut manifest = HashMap::new();
let mut current_section = String::new();
let mut current_subsection = String::new();
let mut indent_level = 0;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let leading_spaces = line.len() - line.trim_start().len();
if let Some(colon_pos) = trimmed.find(':') {
let key = trimmed[..colon_pos].trim();
let value = trimmed[colon_pos + 1..].trim();
if leading_spaces == 0 {
if key == "templates" {
current_section = key.to_string();
indent_level = leading_spaces;
continue;
} else if !value.is_empty() {
manifest.insert(
key.to_string(),
value.trim_matches('"').trim_matches('\'').to_string(),
);
}
} else if leading_spaces > indent_level && current_section == "templates" {
if value.ends_with('/') {
manifest.insert(format!("{}/", key), value.to_string());
} else if value.is_empty() {
current_subsection = key.to_string();
} else {
manifest.insert(key.to_string(), String::new());
}
}
}
else if trimmed.starts_with("- ") {
let filename = trimmed[2..].trim().trim_matches('"').trim_matches('\'');
if !filename.is_empty() {
if current_subsection.is_empty() {
manifest.insert(filename.to_string(), String::new());
} else {
let full_path = format!("{}/{}", current_subsection, filename);
manifest.insert(full_path, String::new());
}
}
}
}
if manifest.is_empty() {
return Err(ManifestError::ParseError(
"No valid entries found in manifest".to_string(),
));
}
Ok(manifest)
}
pub fn list_entries(&self) -> Result<Vec<FileEntry>, ManifestError> {
let manifest = self.fetch_manifest()?;
let mut entries = Vec::new();
for (key, value) in manifest {
if key == "type" {
continue;
}
let is_directory = key.ends_with('/') || value.ends_with('/');
let full_url = if is_directory {
let clean_key = key.trim_end_matches('/');
format!("{}/{}", self.base_url, clean_key)
} else {
format!("{}/{}", self.base_url, key)
};
let clean_name = key.trim_end_matches('/').to_string();
entries.push(FileEntry {
name: clean_name.clone(),
is_directory,
full_url,
});
}
entries.sort_by(|a, b| match (a.is_directory, b.is_directory) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
});
Ok(entries)
}
}