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}