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