use std::collections::HashSet;
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use thiserror::Error;
use crate::models::{Palette, TtpObject};
use crate::parser::parse_stream;
#[derive(Debug, Clone, PartialEq, Error)]
pub enum IncludeError {
#[error("Circular include detected: {}", .0.display())]
CircularInclude(PathBuf),
#[error("Include file not found '{}': {1}", .0.display())]
FileNotFound(PathBuf, String),
#[error("No palette found in included file: {}", .0.display())]
NoPaletteFound(PathBuf),
#[error("Error reading include file '{}': {1}", .0.display())]
IoError(PathBuf, String),
}
pub const INCLUDE_PREFIX: &str = "@include:";
pub fn is_include_ref(palette_ref: &str) -> bool {
palette_ref.starts_with(INCLUDE_PREFIX)
}
pub fn extract_include_path(palette_ref: &str) -> Option<&str> {
palette_ref.strip_prefix(INCLUDE_PREFIX)
}
pub fn resolve_include(include_path: &str, base_path: &Path) -> Result<Palette, IncludeError> {
let mut visited = HashSet::new();
resolve_include_with_detection(include_path, base_path, &mut visited)
}
fn resolve_path_with_extensions(path: &Path) -> Option<PathBuf> {
if path.exists() {
return Some(path.to_path_buf());
}
let alternates = [path.with_extension("pxl"), path.with_extension("jsonl")];
for alt in &alternates {
if alt.exists() {
return Some(alt.clone());
}
}
None
}
pub fn resolve_include_with_detection(
include_path: &str,
base_path: &Path,
visited: &mut HashSet<PathBuf>,
) -> Result<Palette, IncludeError> {
let resolved_path = base_path.join(include_path);
let found_path = resolve_path_with_extensions(&resolved_path).ok_or_else(|| {
IncludeError::FileNotFound(
resolved_path.clone(),
"file not found (tried .pxl and .jsonl extensions)".to_string(),
)
})?;
let canonical_path = found_path
.canonicalize()
.map_err(|e| IncludeError::FileNotFound(found_path.clone(), e.to_string()))?;
if visited.contains(&canonical_path) {
return Err(IncludeError::CircularInclude(canonical_path));
}
visited.insert(canonical_path.clone());
let file = File::open(&canonical_path)
.map_err(|e| IncludeError::IoError(canonical_path.clone(), e.to_string()))?;
let reader = BufReader::new(file);
let parse_result = parse_stream(reader);
let _include_dir = canonical_path.parent().unwrap_or(Path::new("."));
for obj in parse_result.objects {
if let TtpObject::Palette(palette) = obj {
return Ok(palette);
}
}
Err(IncludeError::NoPaletteFound(canonical_path))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_is_include_ref() {
assert!(is_include_ref("@include:path/to/file.jsonl"));
assert!(is_include_ref("@include:./relative.jsonl"));
assert!(!is_include_ref("@gameboy"));
assert!(!is_include_ref("regular_palette"));
assert!(!is_include_ref(""));
}
#[test]
fn test_extract_include_path() {
assert_eq!(extract_include_path("@include:path/to/file.jsonl"), Some("path/to/file.jsonl"));
assert_eq!(extract_include_path("@include:./relative.jsonl"), Some("./relative.jsonl"));
assert_eq!(extract_include_path("@gameboy"), None);
assert_eq!(extract_include_path("regular"), None);
}
#[test]
fn test_resolve_include_simple() {
let temp_dir = TempDir::new().unwrap();
let palette_path = temp_dir.path().join("palette.jsonl");
let mut file = fs::File::create(&palette_path).unwrap();
let content = r##"{"type": "palette", "name": "test", "colors": {"{_}": "#00000000", "{x}": "#FF0000"}}"##;
writeln!(file, "{}", content).unwrap();
let result = resolve_include("palette.jsonl", temp_dir.path());
assert!(result.is_ok());
let palette = result.unwrap();
assert_eq!(palette.name, "test");
assert!(palette.colors.contains_key("{x}"));
}
#[test]
fn test_resolve_include_file_not_found() {
let temp_dir = TempDir::new().unwrap();
let result = resolve_include("nonexistent.jsonl", temp_dir.path());
assert!(result.is_err());
match result.unwrap_err() {
IncludeError::FileNotFound(_, _) => {}
other => panic!("Expected FileNotFound, got {:?}", other),
}
}
#[test]
fn test_resolve_include_no_palette() {
let temp_dir = TempDir::new().unwrap();
let sprite_path = temp_dir.path().join("sprite.jsonl");
let mut file = fs::File::create(&sprite_path).unwrap();
let content = r##"{"type": "sprite", "name": "test", "palette": {"{x}": "#FF0000"}, "grid": ["{x}"]}"##;
writeln!(file, "{}", content).unwrap();
let result = resolve_include("sprite.jsonl", temp_dir.path());
assert!(result.is_err());
match result.unwrap_err() {
IncludeError::NoPaletteFound(_) => {}
other => panic!("Expected NoPaletteFound, got {:?}", other),
}
}
#[test]
fn test_resolve_include_circular_detection() {
let temp_dir = TempDir::new().unwrap();
let palette_path = temp_dir.path().join("palette.jsonl");
let mut file = fs::File::create(&palette_path).unwrap();
let content = r##"{"type": "palette", "name": "test", "colors": {"{x}": "#FF0000"}}"##;
writeln!(file, "{}", content).unwrap();
let mut visited = HashSet::new();
let result1 =
resolve_include_with_detection("palette.jsonl", temp_dir.path(), &mut visited);
assert!(result1.is_ok());
let result2 =
resolve_include_with_detection("palette.jsonl", temp_dir.path(), &mut visited);
assert!(result2.is_err());
match result2.unwrap_err() {
IncludeError::CircularInclude(_) => {}
other => panic!("Expected CircularInclude, got {:?}", other),
}
}
#[test]
fn test_resolve_include_relative_path() {
let temp_dir = TempDir::new().unwrap();
let sub_dir = temp_dir.path().join("shared");
fs::create_dir(&sub_dir).unwrap();
let palette_path = sub_dir.join("colors.jsonl");
let mut file = fs::File::create(&palette_path).unwrap();
let content =
r##"{"type": "palette", "name": "shared_colors", "colors": {"{a}": "#AA0000"}}"##;
writeln!(file, "{}", content).unwrap();
let result = resolve_include("shared/colors.jsonl", temp_dir.path());
assert!(result.is_ok());
let palette = result.unwrap();
assert_eq!(palette.name, "shared_colors");
}
#[test]
fn test_resolve_include_pxl_extension() {
let temp_dir = TempDir::new().unwrap();
let palette_path = temp_dir.path().join("palette.pxl");
let mut file = fs::File::create(&palette_path).unwrap();
let content = r##"{"type": "palette", "name": "pxl_palette", "colors": {"{_}": "#00000000", "{x}": "#00FF00"}}"##;
writeln!(file, "{}", content).unwrap();
let result = resolve_include("palette.pxl", temp_dir.path());
assert!(result.is_ok());
let palette = result.unwrap();
assert_eq!(palette.name, "pxl_palette");
assert!(palette.colors.contains_key("{x}"));
}
#[test]
fn test_resolve_include_extension_auto_detect_pxl() {
let temp_dir = TempDir::new().unwrap();
let palette_path = temp_dir.path().join("colors.pxl");
let mut file = fs::File::create(&palette_path).unwrap();
let content = r##"{"type": "palette", "name": "auto_pxl", "colors": {"{a}": "#0000FF"}}"##;
writeln!(file, "{}", content).unwrap();
let result = resolve_include("colors", temp_dir.path());
assert!(result.is_ok());
let palette = result.unwrap();
assert_eq!(palette.name, "auto_pxl");
}
#[test]
fn test_resolve_include_extension_auto_detect_jsonl() {
let temp_dir = TempDir::new().unwrap();
let palette_path = temp_dir.path().join("colors.jsonl");
let mut file = fs::File::create(&palette_path).unwrap();
let content =
r##"{"type": "palette", "name": "auto_jsonl", "colors": {"{b}": "#FF00FF"}}"##;
writeln!(file, "{}", content).unwrap();
let result = resolve_include("colors", temp_dir.path());
assert!(result.is_ok());
let palette = result.unwrap();
assert_eq!(palette.name, "auto_jsonl");
}
#[test]
fn test_resolve_include_extension_priority_pxl_first() {
let temp_dir = TempDir::new().unwrap();
let pxl_path = temp_dir.path().join("colors.pxl");
let mut pxl_file = fs::File::create(&pxl_path).unwrap();
writeln!(pxl_file, r##"{{"type": "palette", "name": "pxl_wins", "colors": {{}}}}"##)
.unwrap();
let jsonl_path = temp_dir.path().join("colors.jsonl");
let mut jsonl_file = fs::File::create(&jsonl_path).unwrap();
writeln!(jsonl_file, r##"{{"type": "palette", "name": "jsonl_loses", "colors": {{}}}}"##)
.unwrap();
let result = resolve_include("colors", temp_dir.path());
assert!(result.is_ok());
let palette = result.unwrap();
assert_eq!(palette.name, "pxl_wins");
}
#[test]
fn test_resolve_include_subdirectory_pxl() {
let temp_dir = TempDir::new().unwrap();
let sub_dir = temp_dir.path().join("shared");
fs::create_dir(&sub_dir).unwrap();
let palette_path = sub_dir.join("colors.pxl");
let mut file = fs::File::create(&palette_path).unwrap();
let content =
r##"{"type": "palette", "name": "shared_pxl", "colors": {"{c}": "#CCCCCC"}}"##;
writeln!(file, "{}", content).unwrap();
let result = resolve_include("shared/colors.pxl", temp_dir.path());
assert!(result.is_ok());
let palette = result.unwrap();
assert_eq!(palette.name, "shared_pxl");
let result2 = resolve_include("shared/colors", temp_dir.path());
assert!(result2.is_ok());
assert_eq!(result2.unwrap().name, "shared_pxl");
}
}