use std::fs; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::Arc; use crate::GalvanFileExtension; use thiserror::Error; #[derive(Debug, Error)] pub enum FileError { #[error("Error when trying to read source file {0}: {1}")] Io(PathBuf, #[source] std::io::Error), #[error("File name {0} is not valid UTF-8")] Utf8(String), #[error("File name {0} is not allowed. Only lowercase letters and _ are allowed in galvan file names")] Naming(String), #[error("File {0} has no extension")] MissingExtension(PathBuf), } impl FileError { pub fn io(path: impl AsRef, error: std::io::Error) -> Self { Self::Io(path.as_ref().to_owned(), error) } pub fn utf8(file_name: impl Into) -> Self { Self::Utf8(file_name.into()) } pub fn naming(file_name: impl Into) -> Self { Self::Naming(file_name.into()) } pub fn missing_extension(path: impl AsRef) -> Self { Self::MissingExtension(path.as_ref().to_owned()) } } pub type SourceResult = Result; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Source { File { path: Arc, content: Arc, canonical_name: Arc, }, Str(Arc), Missing, Builtin, } impl Source { pub fn from_string(string: impl Into>) -> Source { Self::Str(string.into()) } pub fn read(path: impl AsRef) -> SourceResult { let path = path.as_ref(); if !path.has_galvan_extension() { Err(FileError::missing_extension(path))? } let stem = path .file_stem() .ok_or_else(|| FileError::missing_extension(path))?; let stem = stem .to_str() .ok_or_else(|| FileError::utf8(stem.to_string_lossy()))?; if !stem.chars().all(|c| c.is_ascii_lowercase() || c == '_') { Err(FileError::naming(stem))? } let canonical_name = stem.replace(".", "_").into(); let content = fs::read_to_string(path) .map_err(|e| FileError::io(path, e))? .into(); let path = path.into(); Ok(Self::File { path, content, canonical_name, }) } pub fn content(&self) -> &str { match self { Self::File { content, .. } => content.as_ref(), Self::Str(content) => content.as_ref(), Self::Missing => "", Self::Builtin => "", } } pub fn origin(&self) -> Option<&Path> { match self { Self::File { path, .. } => Some(path), Self::Str(_) => None, Self::Missing => None, Self::Builtin => None, } } pub fn canonical_name(&self) -> Option<&str> { match self { Self::File { path: _, content: _, canonical_name, } => Some(canonical_name), Self::Str(_) => None, Self::Missing => None, Self::Builtin => Some("galvan_std"), } } } impl From for Source where T: Into>, { fn from(value: T) -> Self { Self::from_string(value) } } impl Deref for Source { type Target = str; fn deref(&self) -> &Self::Target { self.content() } }