sea_core/parser/
mod.rs

1use crate::graph::Graph;
2use pest_derive::Parser;
3use std::path::PathBuf;
4
5#[derive(Parser)]
6#[grammar = "grammar/sea.pest"]
7pub struct SeaParser;
8
9pub mod ast;
10pub mod error;
11pub mod lint;
12pub mod printer;
13pub mod profiles;
14pub mod string_utils;
15
16pub use ast::parse_expression_from_str;
17pub use ast::parse_source;
18pub use ast::Ast;
19pub use ast::AstNode;
20pub use error::{ParseError, ParseResult};
21pub use lint::*;
22pub use printer::PrettyPrinter;
23pub use profiles::{Profile, ProfileRegistry};
24pub use string_utils::unescape_string;
25
26/// Controls how the SEA parser interprets declarations.
27///
28/// Currently only the `default_namespace` value influences whether entities/resources
29/// without an explicit namespace inherit a fallback namespace.
30#[derive(Debug, Clone, Default)]
31pub struct ParseOptions {
32    /// When `Some`, unqualified declarations receive this namespace.
33    /// When `None`, entities and resources must provide their own namespace.
34    pub default_namespace: Option<String>,
35    /// Optional namespace registry used for module resolution.
36    pub namespace_registry: Option<crate::registry::NamespaceRegistry>,
37    /// Path to the entry file being parsed, used for module resolution.
38    pub entry_path: Option<PathBuf>,
39    /// Active profile to enforce when no profile is declared in the source file.
40    pub active_profile: Option<String>,
41    /// Downgrades profile violations from hard errors to warnings when set.
42    pub tolerate_profile_warnings: bool,
43}
44
45/// Parse SEA DSL source code into an AST
46pub fn parse(source: &str) -> ParseResult<Ast> {
47    ast::parse_source(source)
48}
49
50/// Parse SEA DSL source code directly into a Graph
51pub fn parse_to_graph(source: &str) -> ParseResult<Graph> {
52    let ast = parse(source)?;
53    ast::ast_to_graph_with_options(ast, &ParseOptions::default())
54}
55
56/// Parses SEA DSL `source` directly into a `Graph`, honoring the provided `options`.
57///
58/// # Parameters
59/// - `source`: DSL text that will be parsed into AST nodes.
60/// - `options`: Controls parser behavior (`default_namespace`, module resolution via `namespace_registry`/`entry_path`, `active_profile`, and whether profile violations are warnings).
61///
62/// # Returns
63/// A `ParseResult` containing the constructed `Graph` or a `ParseError`.
64///
65/// # Errors
66/// Returns an error if parsing fails or AST validation (graph construction) rejects the input.
67///
68/// # Example
69/// ```
70/// use sea_core::parser::{parse_to_graph_with_options, ParseOptions};
71///
72/// let options = ParseOptions {
73///     default_namespace: Some("logistics".to_string()),
74///     ..Default::default()
75/// };
76/// let graph = parse_to_graph_with_options("Entity \"Warehouse\"", &options).unwrap();
77/// ```
78pub fn parse_to_graph_with_options(source: &str, options: &ParseOptions) -> ParseResult<Graph> {
79    match (&options.namespace_registry, &options.entry_path) {
80        (Some(registry), Some(path)) => {
81            let mut resolver = crate::module::resolver::ModuleResolver::new(registry)?;
82            let ast = resolver.validate_entry(path, source)?;
83            resolver.validate_dependencies(path, &ast)?;
84            ast::ast_to_graph_with_options(ast, options)
85        }
86        (Some(_), None) => Err(ParseError::Validation(
87            "Namespace registry provided without entry path".to_string(),
88        )),
89        _ => {
90            if let Some(path) = &options.entry_path {
91                log::warn!(
92                        "Entry path '{}' provided without namespace registry; module resolution skipped",
93                        path.display()
94                    );
95            }
96            let ast = parse(source)?;
97            ast::ast_to_graph_with_options(ast, options)
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_entity_declaration_syntax() {
108        let source = r#"
109            Entity "Warehouse A" in logistics
110        "#;
111
112        let result = parse(source);
113        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
114    }
115
116    #[test]
117    fn test_resource_declaration_syntax() {
118        let source = r#"
119            Resource "Camera Units" units in inventory
120        "#;
121
122        let result = parse(source);
123        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
124    }
125
126    #[test]
127    fn test_flow_declaration_syntax() {
128        let source = r#"
129            Flow "Camera Units" from "Warehouse" to "Factory" quantity 100
130        "#;
131
132        let result = parse(source);
133        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
134    }
135
136    #[test]
137    fn test_policy_simple_syntax() {
138        let source = r#"
139            Policy check_quantity as: Flow.quantity > 0
140        "#;
141
142        let result = parse(source);
143        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
144    }
145
146    #[test]
147    fn test_complex_policy_syntax() {
148        let source = r#"
149            Policy flow_constraints as:
150                (Flow.quantity > 0) and (Entity.name != "")
151        "#;
152
153        let result = parse(source);
154        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
155    }
156
157    #[test]
158    fn test_nested_expressions() {
159        let source = r#"
160            Policy multi_condition as:
161                (A or B) and (C or (D and E))
162        "#;
163
164        let result = parse(source);
165        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
166    }
167
168    #[test]
169    fn test_comments_ignored() {
170        let source = r#"
171            // This is a comment
172            Entity "Test" in domain
173            // Another comment
174        "#;
175
176        let result = parse(source);
177        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
178    }
179
180    #[test]
181    fn test_multiple_declarations() {
182        let source = r#"
183            Entity "Warehouse" in logistics
184            Resource "Cameras" units
185            Flow "Cameras" from "Warehouse" to "Factory" quantity 50
186        "#;
187
188        let result = parse(source);
189        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
190    }
191}