1use std::fmt;
2use std::fs;
3use std::io;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug)]
8pub struct FilesystemProject {
9 source: String,
10 main_path: PathBuf,
11 root: PathBuf,
12}
13
14impl FilesystemProject {
15 pub fn load(path: &Path) -> Result<Self, FilesystemProjectError> {
17 let canonical_target = path
18 .canonicalize()
19 .map_err(|error| FilesystemProjectError::entry_not_found(path.to_path_buf(), error))?;
20 let selected_path = if canonical_target.is_dir() {
21 default_entry(&canonical_target)?
22 } else {
23 canonical_target.clone()
24 };
25 let source = fs::read_to_string(&selected_path).map_err(|error| {
26 FilesystemProjectError::entry_unreadable(selected_path.clone(), error)
27 })?;
28 let root = selected_path
29 .parent()
30 .map(Path::to_path_buf)
31 .unwrap_or_else(|| PathBuf::from("."));
32 Ok(Self {
33 source,
34 main_path: if canonical_target.is_dir() {
35 selected_path
36 } else {
37 path.to_path_buf()
38 },
39 root,
40 })
41 }
42
43 pub fn source(&self) -> &str {
45 &self.source
46 }
47
48 pub fn main_path(&self) -> &Path {
50 &self.main_path
51 }
52
53 pub fn root(&self) -> &Path {
55 &self.root
56 }
57}
58
59fn default_entry(directory: &Path) -> Result<PathBuf, FilesystemProjectError> {
60 let candidates = [directory.join("main.opy"), directory.join("src/main.opy")];
61 candidates
62 .into_iter()
63 .find(|candidate| candidate.is_file())
64 .ok_or_else(|| FilesystemProjectError::default_entry_not_found(directory.to_path_buf()))
65}
66
67#[derive(Debug)]
69pub enum FilesystemProjectError {
70 EntryNotFound { path: PathBuf, source: io::Error },
71 EntryUnreadable { path: PathBuf, source: io::Error },
72 DefaultEntryNotFound { path: PathBuf },
73}
74
75impl FilesystemProjectError {
76 fn entry_not_found(path: PathBuf, source: io::Error) -> Self {
77 Self::EntryNotFound { path, source }
78 }
79
80 fn entry_unreadable(path: PathBuf, source: io::Error) -> Self {
81 Self::EntryUnreadable { path, source }
82 }
83
84 fn default_entry_not_found(path: PathBuf) -> Self {
85 Self::DefaultEntryNotFound { path }
86 }
87
88 pub fn is_entry_not_found(&self) -> bool {
90 matches!(
91 self,
92 Self::EntryNotFound { .. } | Self::DefaultEntryNotFound { .. }
93 )
94 }
95}
96
97impl fmt::Display for FilesystemProjectError {
98 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 Self::EntryNotFound { source, .. } => write!(formatter, "cannot find entry: {source}"),
101 Self::EntryUnreadable { source, .. } => {
102 write!(formatter, "cannot read entry: {source}")
103 }
104 Self::DefaultEntryNotFound { path } => {
105 write!(
106 formatter,
107 "cannot find a default OPY entry in '{}'; expected main.opy or src/main.opy",
108 path.display()
109 )
110 }
111 }
112 }
113}
114
115impl std::error::Error for FilesystemProjectError {
116 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
117 match self {
118 Self::EntryNotFound { source, .. } | Self::EntryUnreadable { source, .. } => {
119 Some(source)
120 }
121 Self::DefaultEntryNotFound { .. } => None,
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::FilesystemProject;
129
130 #[test]
131 fn directory_targets_use_the_owner_default_entry() {
132 let directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
133 .join("tests/fixtures/project-preprocessing");
134 let project = FilesystemProject::load(&directory).expect("directory project loads");
135 assert!(project.main_path().ends_with("main.opy"));
136 assert_eq!(project.root(), directory.canonicalize().unwrap());
137 assert!(project.source().contains("rule \"main\""));
138 }
139}