galvan_files/
source.rs

1use std::fs;
2use std::ops::Deref;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use crate::GalvanFileExtension;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum FileError {
11    #[error("Error when trying to read source file {0}: {1}")]
12    Io(PathBuf, #[source] std::io::Error),
13    #[error("File name {0} is not valid UTF-8")]
14    Utf8(String),
15    #[error("File name {0} is not allowed. Only lowercase letters and _ are allowed in galvan file names")]
16    Naming(String),
17    #[error("File {0} has no extension")]
18    MissingExtension(PathBuf),
19}
20
21impl FileError {
22    pub fn io(path: impl AsRef<Path>, error: std::io::Error) -> Self {
23        Self::Io(path.as_ref().to_owned(), error)
24    }
25
26    pub fn utf8(file_name: impl Into<String>) -> Self {
27        Self::Utf8(file_name.into())
28    }
29
30    pub fn naming(file_name: impl Into<String>) -> Self {
31        Self::Naming(file_name.into())
32    }
33
34    pub fn missing_extension(path: impl AsRef<Path>) -> Self {
35        Self::MissingExtension(path.as_ref().to_owned())
36    }
37}
38
39pub type SourceResult = Result<Source, FileError>;
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum Source {
43    File {
44        path: Arc<Path>,
45        content: Arc<str>,
46        canonical_name: Arc<str>,
47    },
48    Str(Arc<str>),
49    Missing,
50    Builtin,
51}
52
53impl Source {
54    pub fn from_string(string: impl Into<Arc<str>>) -> Source {
55        Self::Str(string.into())
56    }
57
58    pub fn read(path: impl AsRef<Path>) -> SourceResult {
59        let path = path.as_ref();
60        if !path.has_galvan_extension() {
61            Err(FileError::missing_extension(path))?
62        }
63        let stem = path
64            .file_stem()
65            .ok_or_else(|| FileError::missing_extension(path))?;
66
67        let stem = stem
68            .to_str()
69            .ok_or_else(|| FileError::utf8(stem.to_string_lossy()))?;
70        if !stem.chars().all(|c| c.is_ascii_lowercase() || c == '_') {
71            Err(FileError::naming(stem))?
72        }
73        let canonical_name = stem.replace(".", "_").into();
74        let content = fs::read_to_string(path)
75            .map_err(|e| FileError::io(path, e))?
76            .into();
77        let path = path.into();
78
79        Ok(Self::File {
80            path,
81            content,
82            canonical_name,
83        })
84    }
85
86    pub fn content(&self) -> &str {
87        match self {
88            Self::File { content, .. } => content.as_ref(),
89            Self::Str(content) => content.as_ref(),
90            Self::Missing => "",
91            Self::Builtin => "",
92        }
93    }
94
95    pub fn origin(&self) -> Option<&Path> {
96        match self {
97            Self::File { path, .. } => Some(path),
98            Self::Str(_) => None,
99            Self::Missing => None,
100            Self::Builtin => None,
101        }
102    }
103
104    pub fn canonical_name(&self) -> Option<&str> {
105        match self {
106            Self::File {
107                path: _,
108                content: _,
109                canonical_name,
110            } => Some(canonical_name),
111            Self::Str(_) => None,
112            Self::Missing => None,
113            Self::Builtin => Some("galvan_std"),
114        }
115    }
116}
117
118impl<T> From<T> for Source
119where
120    T: Into<Arc<str>>,
121{
122    fn from(value: T) -> Self {
123        Self::from_string(value)
124    }
125}
126
127impl Deref for Source {
128    type Target = str;
129    fn deref(&self) -> &Self::Target {
130        self.content()
131    }
132}