Skip to main content

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}