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::{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}