Skip to main content

plissken_core/parser/
traits.rs

1//! Parser trait and related types for language-agnostic parsing.
2//!
3//! This module defines the `Parser` trait which provides a common interface
4//! for parsing source code from different languages.
5
6use crate::error::Result;
7use crate::model::{PythonModule, RustModule};
8use std::path::Path;
9
10/// Language identifier for parsers.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ParserLanguage {
13    /// Rust programming language
14    Rust,
15    /// Python programming language
16    Python,
17}
18
19impl std::fmt::Display for ParserLanguage {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            ParserLanguage::Rust => write!(f, "Rust"),
23            ParserLanguage::Python => write!(f, "Python"),
24        }
25    }
26}
27
28/// A parsed module, either Rust or Python.
29///
30/// This enum allows parsers to return a unified type while preserving
31/// the specific module information for each language.
32#[derive(Debug, Clone)]
33pub enum Module {
34    /// A parsed Rust module
35    Rust(RustModule),
36    /// A parsed Python module
37    Python(PythonModule),
38}
39
40impl Module {
41    /// Get the module path.
42    pub fn path(&self) -> &str {
43        match self {
44            Module::Rust(m) => &m.path,
45            Module::Python(m) => &m.path,
46        }
47    }
48
49    /// Get the language of this module.
50    pub fn language(&self) -> ParserLanguage {
51        match self {
52            Module::Rust(_) => ParserLanguage::Rust,
53            Module::Python(_) => ParserLanguage::Python,
54        }
55    }
56
57    /// Try to get as a Rust module.
58    pub fn as_rust(&self) -> Option<&RustModule> {
59        match self {
60            Module::Rust(m) => Some(m),
61            Module::Python(_) => None,
62        }
63    }
64
65    /// Try to get as a Python module.
66    pub fn as_python(&self) -> Option<&PythonModule> {
67        match self {
68            Module::Rust(_) => None,
69            Module::Python(m) => Some(m),
70        }
71    }
72
73    /// Convert into a Rust module, if applicable.
74    pub fn into_rust(self) -> Option<RustModule> {
75        match self {
76            Module::Rust(m) => Some(m),
77            Module::Python(_) => None,
78        }
79    }
80
81    /// Convert into a Python module, if applicable.
82    pub fn into_python(self) -> Option<PythonModule> {
83        match self {
84            Module::Rust(_) => None,
85            Module::Python(m) => Some(m),
86        }
87    }
88}
89
90impl From<RustModule> for Module {
91    fn from(m: RustModule) -> Self {
92        Module::Rust(m)
93    }
94}
95
96impl From<PythonModule> for Module {
97    fn from(m: PythonModule) -> Self {
98        Module::Python(m)
99    }
100}
101
102/// A language-specific documentation parser.
103///
104/// This trait provides a unified interface for parsing source code from
105/// different programming languages. Implementations handle the language-specific
106/// parsing logic while exposing a common API.
107///
108/// # Example
109///
110/// ```ignore
111/// use plissken_core::parser::{Parser, ParserLanguage, create_parser};
112///
113/// // Create a parser dynamically
114/// let mut parser = create_parser(ParserLanguage::Rust);
115/// let module = parser.parse_file(Path::new("src/lib.rs"))?;
116///
117/// // Or use concrete types directly
118/// let mut rust_parser = RustParser::new();
119/// let rust_module = rust_parser.parse_file(Path::new("src/lib.rs"))?;
120/// ```
121pub trait Parser: Send {
122    /// Parse a source file and return its module representation.
123    ///
124    /// # Arguments
125    ///
126    /// * `path` - Path to the source file to parse
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if the file cannot be read or contains invalid syntax.
131    fn parse_file(&mut self, path: &Path) -> Result<Module>;
132
133    /// Parse source code from a string.
134    ///
135    /// This is useful for testing or when source code is available in memory.
136    ///
137    /// # Arguments
138    ///
139    /// * `content` - The source code to parse
140    /// * `virtual_path` - A path to use for error messages and source locations
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if the source contains invalid syntax.
145    fn parse_str(&mut self, content: &str, virtual_path: &Path) -> Result<Module>;
146
147    /// The language this parser handles.
148    fn language(&self) -> ParserLanguage;
149
150    /// Human-readable name for error messages.
151    fn name(&self) -> &'static str;
152
153    /// File extensions this parser handles.
154    fn extensions(&self) -> &'static [&'static str];
155
156    /// Check if this parser can handle the given file extension.
157    fn can_parse_extension(&self, ext: &str) -> bool {
158        self.extensions()
159            .iter()
160            .any(|e| e.eq_ignore_ascii_case(ext))
161    }
162}
163
164/// Create a parser for the given language.
165///
166/// # Example
167///
168/// ```ignore
169/// use plissken_core::parser::{create_parser, ParserLanguage};
170///
171/// let mut parser = create_parser(ParserLanguage::Python);
172/// let module = parser.parse_file(Path::new("module.py"))?;
173/// ```
174pub fn create_parser(language: ParserLanguage) -> Box<dyn Parser> {
175    match language {
176        ParserLanguage::Rust => Box::new(super::RustParser::new()),
177        ParserLanguage::Python => Box::new(super::PythonParser::new()),
178    }
179}
180
181/// Get a parser for the given file extension.
182///
183/// Returns `None` if the extension is not recognized.
184///
185/// # Example
186///
187/// ```ignore
188/// use plissken_core::parser::parser_for_extension;
189///
190/// if let Some(mut parser) = parser_for_extension("py") {
191///     let module = parser.parse_file(Path::new("module.py"))?;
192/// }
193/// ```
194pub fn parser_for_extension(ext: &str) -> Option<Box<dyn Parser>> {
195    match ext.to_lowercase().as_str() {
196        "rs" => Some(Box::new(super::RustParser::new())),
197        "py" | "pyi" => Some(Box::new(super::PythonParser::new())),
198        _ => None,
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_parser_language_display() {
208        assert_eq!(ParserLanguage::Rust.to_string(), "Rust");
209        assert_eq!(ParserLanguage::Python.to_string(), "Python");
210    }
211
212    #[test]
213    fn test_module_conversions() {
214        let rust_module = RustModule::test("crate::test");
215        let module: Module = rust_module.into();
216
217        assert!(matches!(module.language(), ParserLanguage::Rust));
218        assert!(module.as_rust().is_some());
219        assert!(module.as_python().is_none());
220    }
221
222    #[test]
223    fn test_create_parser() {
224        let rust_parser = create_parser(ParserLanguage::Rust);
225        assert_eq!(rust_parser.language(), ParserLanguage::Rust);
226        assert_eq!(rust_parser.name(), "Rust");
227        assert!(rust_parser.extensions().contains(&"rs"));
228
229        let python_parser = create_parser(ParserLanguage::Python);
230        assert_eq!(python_parser.language(), ParserLanguage::Python);
231        assert_eq!(python_parser.name(), "Python");
232        assert!(python_parser.extensions().contains(&"py"));
233    }
234
235    #[test]
236    fn test_parser_for_extension() {
237        assert!(parser_for_extension("rs").is_some());
238        assert!(parser_for_extension("RS").is_some()); // case insensitive
239        assert!(parser_for_extension("py").is_some());
240        assert!(parser_for_extension("pyi").is_some());
241        assert!(parser_for_extension("js").is_none());
242        assert!(parser_for_extension("").is_none());
243    }
244
245    #[test]
246    fn test_can_parse_extension() {
247        let parser = create_parser(ParserLanguage::Rust);
248        assert!(parser.can_parse_extension("rs"));
249        assert!(parser.can_parse_extension("RS"));
250        assert!(!parser.can_parse_extension("py"));
251    }
252}