multilinear_parser/extended.rs
1use std::{
2 fs::{File, read_dir},
3 io::{BufRead, BufReader},
4 path::{Path, PathBuf},
5};
6
7use super::{AspectAddingError, Error, MultilinearParser, NamedMultilinearInfo};
8
9/// Represents the kind of errors that can occur during aspect file parsing.
10#[derive(Error, Debug)]
11pub enum AspectErrorKind {
12 /// Failed to open the aspects configuration file.
13 #[error("Failed to open multilinear aspect file")]
14 OpeningFile,
15 /// Failed to read lines from the aspects configuration file.
16 #[error("Failed to read multilinear aspect file")]
17 ReadingFile,
18 /// Error while adding a default aspect from the aspects file.
19 #[error("Error adding default aspect: {0}")]
20 AddingAspect(#[source] AspectAddingError),
21 /// An invalid aspect default was found in the aspects file.
22 #[error("Invalid aspect default: {0}")]
23 InvalidAspectDefault(Box<str>),
24}
25
26impl AspectErrorKind {
27 fn error(self, path: PathBuf) -> AspectError {
28 AspectError { path, kind: self }
29 }
30}
31
32/// Represents errors that can occur during aspect file parsing.
33#[derive(Error, Debug)]
34#[error("Error parsing aspect file \"{path}\": {kind}", path = path.display())]
35pub struct AspectError {
36 path: PathBuf,
37 kind: AspectErrorKind,
38}
39
40/// Represents the kind of errors that can occur during directory or file parsing.
41#[derive(Error, Debug)]
42pub enum DirectoryOrFileErrorKind {
43 /// The specified path does not exist or cannot be accessed.
44 #[error("Path does not exist")]
45 PathNotFound,
46 /// Failed to open a multilinear definition file for reading.
47 #[error("Failed to open multilinear definition file")]
48 OpeningFile,
49 /// A parsing error occurred within a specific file.
50 #[error("Parsing error: {0}")]
51 Parsing(#[source] Error),
52 /// Failed to read directory contents.
53 #[error("Failed to read directory")]
54 ReadingDirectory,
55}
56
57impl DirectoryOrFileErrorKind {
58 fn error(self, path: PathBuf) -> DirectoryOrFileError {
59 DirectoryOrFileError { path, kind: self }
60 }
61}
62
63/// Represents errors that can occur during directory or file parsing.
64#[derive(Error, Debug)]
65#[error("Error parsing \"{path}\": {kind}", path = path.display())]
66pub struct DirectoryOrFileError {
67 path: PathBuf,
68 kind: DirectoryOrFileErrorKind,
69}
70
71/// Represents errors that can occur during extended parsing operations.
72///
73/// This includes filesystem errors and aspect initialization errors.
74#[derive(Error, Debug)]
75pub enum ExtendedError {
76 /// Error while parsing directory or file.
77 #[error("{0}")]
78 DirectoryOrFile(
79 #[source]
80 #[from]
81 DirectoryOrFileError,
82 ),
83 /// Error while parsing the aspect file.
84 #[error("{0}")]
85 AspectFile(
86 #[source]
87 #[from]
88 AspectError,
89 ),
90}
91
92impl MultilinearParser {
93 /// Parses aspect defaults from a file without parsing story content.
94 ///
95 /// The format for each line is `aspect_name: default_value`.
96 /// Empty lines are ignored.
97 ///
98 /// # Arguments
99 ///
100 /// - `path` - Path to the aspects file (typically with `.mla` extension)
101 ///
102 /// # Errors
103 ///
104 /// Returns `AspectError` if the file cannot be opened, read, or contains invalid data.
105 pub fn parse_aspect_defaults(&mut self, path: &Path) -> Result<(), AspectError> {
106 let Ok(file) = File::open(path) else {
107 return Err(AspectErrorKind::OpeningFile.error(path.into()));
108 };
109 for line in BufReader::new(file).lines() {
110 let Ok(line) = line else {
111 return Err(AspectErrorKind::ReadingFile.error(path.into()));
112 };
113 if let Some((aspect, default_value)) = line.split_once(':') {
114 if let Err(err) = self.add_new_aspect(aspect, default_value) {
115 return Err(AspectErrorKind::AddingAspect(err).error(path.into()));
116 }
117 continue;
118 }
119 let line = line.trim();
120 if line.is_empty() {
121 continue;
122 }
123 return Err(AspectErrorKind::InvalidAspectDefault(line.into()).error(path.into()));
124 }
125 Ok(())
126 }
127
128 /// Recursively parses multilinear definitions from a file or directory tree.
129 ///
130 /// If `path` is a file, it will be parsed directly (if it has the `.mld` extension).
131 /// If `path` is a directory, all `.mld` files and subdirectories will be processed recursively.
132 ///
133 /// # Namespace Building
134 ///
135 /// When traversing directories, the directory names are appended to the namespace,
136 /// creating a hierarchical structure. For example, a file at `chapters/intro/scene.mld`
137 /// will receive the namespace `["chapters", "intro"]` in addition to any existing namespace.
138 ///
139 /// # Arguments
140 ///
141 /// - `path` - Path to a `.mld` file or directory containing `.mld` files
142 /// - `namespace` - The current namespace stack (directory names are appended during traversal)
143 ///
144 /// # Errors
145 ///
146 /// Returns `DirectoryOrFileError` of this kind:
147 /// - `PathNotFound` if the path doesn't exist
148 /// - `OpeningFile` if a file cannot be opened
149 /// - `Parsing` if a file contains invalid data
150 /// - `ReadingDirectory` if a directory cannot be read
151 ///
152 /// # Example
153 ///
154 /// ```no_run
155 /// use std::path::Path;
156 /// use multilinear_parser::MultilinearParser;
157 ///
158 /// let mut parser = MultilinearParser::default();
159 /// let mut namespace = Vec::new();
160 /// parser.parse_directory_or_file(Path::new("story/"), &mut namespace).unwrap();
161 /// ```
162 pub fn parse_directory_or_file(
163 &mut self,
164 path: &Path,
165 namespace: &mut Vec<Box<str>>,
166 ) -> Result<(), DirectoryOrFileError> {
167 if !path.exists() {
168 return Err(DirectoryOrFileErrorKind::PathNotFound.error(path.into()));
169 }
170 if path.is_file() {
171 let valid_path = path.extension().is_some_and(|e| e == "mld");
172 if !valid_path {
173 return Ok(());
174 }
175 let Ok(multilinear_file) = File::open(path) else {
176 return Err(DirectoryOrFileErrorKind::OpeningFile.error(path.into()));
177 };
178 if let Err(source) = self.parse(multilinear_file, namespace.as_ref()) {
179 return Err(DirectoryOrFileErrorKind::Parsing(source).error(path.into()));
180 }
181 return Ok(());
182 }
183 let Ok(dir) = read_dir(path) else {
184 return Err(DirectoryOrFileErrorKind::ReadingDirectory.error(path.into()));
185 };
186 for file in dir.flatten() {
187 let path = file.path();
188 let Some(name) = path.file_stem() else {
189 continue;
190 };
191 let Some(name) = name.to_str() else { continue };
192 namespace.push(name.into());
193 let result = self.parse_directory_or_file(&path, namespace);
194 namespace.pop();
195 result?;
196 }
197 Ok(())
198 }
199}
200
201/// Parses a multilinear system from a directory or file, with optional aspect initialization.
202///
203/// This is a convenience function that creates a parser, optionally loads aspect defaults
204/// via [`parse_aspect_defaults`](MultilinearParser::parse_aspect_defaults), and then
205/// processes the input path via [`parse_directory_or_file`](MultilinearParser::parse_directory_or_file).
206///
207/// # Arguments
208///
209/// - `input_path` - Path to a `.mld` file or directory to parse
210/// - `aspects_path` - Optional path to an aspects file defining initial conditions
211/// Each line should be in the format `aspect_name:default_value`.
212///
213/// # Returns
214///
215/// Returns the fully parsed `NamedMultilinearInfo` on success.
216///
217/// # Errors
218///
219/// Returns `ExtendedError` variants for file system errors, parsing errors, or invalid aspect definitions.
220///
221/// # Example
222///
223/// ```no_run
224/// use std::path::Path;
225/// use multilinear_parser::parse_multilinear_extended;
226///
227/// // Parse a directory tree with custom aspect default values
228/// let story = parse_multilinear_extended(
229/// "story/chapters/".as_ref(),
230/// Some("story/aspects.mla".as_ref())
231/// ).unwrap();
232///
233/// // Or parse a single file without aspects
234/// let story = parse_multilinear_extended(
235/// "story.mld".as_ref(),
236/// None
237/// ).unwrap();
238/// ```
239pub fn parse_multilinear_extended(
240 input_path: &Path,
241 aspects_path: Option<&Path>,
242) -> Result<NamedMultilinearInfo, ExtendedError> {
243 let mut multilinear_parser = MultilinearParser::default();
244 if let Some(aspects_path) = &aspects_path {
245 multilinear_parser.parse_aspect_defaults(aspects_path)?;
246 }
247 multilinear_parser.parse_directory_or_file(input_path, &mut Vec::new())?;
248 Ok(multilinear_parser.into_info())
249}