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