mon_core/
api.rs

1//! # Public API for MON Core
2//!
3//! This module provides the primary high-level interface for the `mon-core` library. It is the
4//! recommended entry point for most users who want to parse, resolve, and validate MON source code
5//! in a single, streamlined operation.
6//!
7//! ## Architectural Overview
8//!
9//! The `api` module acts as a facade over the entire compilation pipeline, orchestrating the
10//! [`lexer`](crate::lexer), [`parser`](crate::parser), and [`resolver`](crate::resolver)
11//! to provide a simple and powerful analysis function. Its main purpose is to abstract away the
12//! complexities of the individual stages.
13//!
14//! ## Use Cases
15//!
16//! The central use case is to take a MON source string and get a fully processed, validated,
17//! and serializable result.
18//!
19//! - **Analyzing a MON file:** The [`analyze`] function is the workhorse of this module.
20//! - **Serializing the result:** Once analysis is complete, the [`AnalysisResult`] can be
21//!   easily converted to other formats like JSON or YAML.
22//! - **Powering Language Tools:** For more advanced use cases like Language Server Protocol (LSP)
23//!   implementations, the `AnalysisResult` provides methods for "go to definition", "hover",
24//!   and "find references" when the `lsp` feature is enabled.
25//!
26//! ## Example: Analyze and Serialize
27//!
28//! ```rust
29//! use mon_core::api::analyze;
30//! # use mon_core::error::MonError;
31//!
32//! # fn main() -> Result<(), MonError> {
33//! let source = r#"{ version: 1.0, features: ["a", "b"] }"#;
34//!
35//! // 1. Analyze the source string.
36//! let result = analyze(source, "my_config.mon")?;
37//!
38//! // 2. Convert the result to a YAML string.
39//! let yaml_output = result.to_yaml().unwrap();
40//!
41//! println!("{}", yaml_output);
42//! assert!(yaml_output.contains("version: 1.0"));
43//! # Ok(())
44//! # }
45//! ```
46#[allow(dead_code)]
47use crate::ast::{MonDocument, MonValue, SymbolTable, TypeSpec};
48use crate::error::MonError;
49
50#[cfg(feature = "lsp")]
51use crate::ast::{Member, MonValueKind};
52#[cfg(feature = "lsp")]
53use crate::lsp;
54use crate::parser::Parser;
55use crate::resolver::Resolver;
56use crate::serialization::{to_value, Value};
57#[cfg(feature = "lsp")]
58use miette::SourceSpan;
59use serde::{Serialize, Serializer};
60use serde_json;
61use serde_yaml;
62use std::collections::HashMap;
63use std::fmt::Display;
64use std::path::PathBuf;
65
66/// The result of a successful analysis of a MON document.
67///
68/// This struct contains the fully resolved [`MonDocument`] and provides
69/// methods for serialization and further inspection, making it
70/// suitable for both direct consumption and for powering an LSP.
71pub struct AnalysisResult {
72    /// The fully resolved and validated [`MonDocument`].
73    pub document: MonDocument,
74    /// A clone of the original [`MonDocument`] before resolution, used for LSP features.
75    pub unresolved_document: MonDocument,
76    /// The symbol table containing all type definitions.
77    pub symbol_table: SymbolTable,
78    /// A map of all declared anchors.
79    pub anchors: HashMap<String, MonValue>,
80}
81
82impl Serialize for AnalysisResult {
83    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
84    where
85        S: Serializer,
86    {
87        let value = self.to_value();
88        value.serialize(serializer)
89    }
90}
91
92impl AnalysisResult {
93    /// Serializes the resolved MON data into a generic, serializable `Value`.
94    #[must_use]
95    pub fn to_value(&self) -> Value {
96        to_value(&self.document.root)
97    }
98
99    /// Serializes the resolved MON data into a pretty-printed JSON string.
100    ///
101    /// # Errors
102    /// Returns a `serde_json::Error` if serialization fails.
103    pub fn to_json(&self) -> Result<String, serde_json::Error> {
104        serde_json::to_string_pretty(&self)
105    }
106
107    /// Serializes the resolved MON data into a YAML string.
108    ///
109    /// # Errors
110    /// Returns a `serde_yaml::Error` if serialization fails.
111    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
112        serde_yaml::to_string(&self)
113    }
114
115    #[cfg(feature = "lsp")]
116    /// Finds the definition of the symbol at the given character position.
117    /// This is the core of "go to definition".
118    #[must_use]
119    pub fn get_definition_at(&self, position: usize) -> Option<SourceSpan> {
120        let node = find_node_at(&self.unresolved_document.root, position)?;
121
122        match node {
123            FoundNode::Value(value) => match &value.kind {
124                MonValueKind::Alias(alias_name) => {
125                    let anchor_def = self.anchors.get(alias_name)?;
126                    Some(anchor_def.get_source_span())
127                }
128                _ => None,
129            },
130            FoundNode::TypeSpec(type_spec) => match type_spec {
131                TypeSpec::Simple(name, _) => {
132                    let type_def = self.symbol_table.types.get(name)?;
133                    Some(type_def.def_type.get_span())
134                }
135                _ => None,
136            },
137        }
138    }
139
140    #[cfg(feature = "lsp")]
141    /// Gets information about the type of the symbol at the given character position.
142    /// This is the core of "hover" tooltips.
143    #[must_use]
144    pub fn get_type_info_at(&self, position: usize) -> Option<String> {
145        let symbol_info = lsp::find_symbol_at(&self.unresolved_document.root, position)?;
146
147        if let Some(validation) = symbol_info.validation {
148            return Some(validation.to_string());
149        }
150
151        match symbol_info.node {
152            lsp::FoundNode::Value(value) => Some(value.kind.to_string()),
153            lsp::FoundNode::TypeSpec(type_spec) => Some(type_spec.to_string()),
154        }
155    }
156    #[cfg(feature = "lsp")]
157    /// Finds all references to the symbol at the given character position.
158    pub fn find_references(&self, position: usize) -> Option<Vec<SourceSpan>> {
159        let symbol_info = lsp::find_symbol_at(&self.unresolved_document.root, position)?;
160
161        let (name_to_find, definition_span) = match symbol_info.node {
162            lsp::FoundNode::Value(value) => match &value.kind {
163                MonValueKind::Alias(alias_name) => {
164                    let anchor_def = self.anchors.get(alias_name)?;
165                    (alias_name.clone(), anchor_def.get_source_span())
166                }
167                _ => return None,
168            },
169            lsp::FoundNode::TypeSpec(type_spec) => match type_spec {
170                TypeSpec::Simple(name, _) => {
171                    let type_def = self.symbol_table.types.get(name)?;
172                    (name.clone(), type_def.name_span)
173                }
174                _ => return None,
175            },
176        };
177
178        let usages = lsp::find_all_usages(&self.unresolved_document.root, &name_to_find)
179            .into_iter()
180            .filter(|span| *span != definition_span)
181            .collect();
182        Some(usages)
183    }
184}
185
186#[cfg(feature = "lsp")]
187#[derive(Debug, Clone, Copy)]
188/// An AST node found during a location-based query.
189enum FoundNode<'a> {
190    Value(&'a MonValue),
191    TypeSpec(&'a TypeSpec),
192}
193
194#[cfg(feature = "lsp")]
195/// Finds the most specific AST node that contains the given character position.
196fn find_node_at(value: &MonValue, position: usize) -> Option<FoundNode<'_>> {
197    if position < value.pos_start || position >= value.pos_end {
198        return None;
199    }
200
201    if let MonValueKind::Object(members) = &value.kind {
202        for member in members {
203            if let Member::Pair(pair) = member {
204                if let Some(validation) = &pair.validation {
205                    if let Some(found) = find_node_in_type_spec(validation, position) {
206                        return Some(found);
207                    }
208                }
209                if let Some(found) = find_node_at(&pair.value, position) {
210                    return Some(found);
211                }
212            }
213        }
214    }
215
216    if let MonValueKind::Array(elements) = &value.kind {
217        for element in elements {
218            if let Some(found) = find_node_at(element, position) {
219                return Some(found);
220            }
221        }
222    }
223
224    Some(FoundNode::Value(value))
225}
226
227#[cfg(feature = "lsp")]
228/// Recursively finds the most specific `TypeSpec` node containing the given position.
229fn find_node_in_type_spec(type_spec: &TypeSpec, position: usize) -> Option<FoundNode<'_>> {
230    let span = type_spec.get_span();
231    if position < span.offset() || position >= span.offset() + span.len() {
232        return None;
233    }
234
235    if let TypeSpec::Collection(children, _) = type_spec {
236        for child in children {
237            if let Some(found) = find_node_in_type_spec(child, position) {
238                return Some(found);
239            }
240        }
241    }
242
243    Some(FoundNode::TypeSpec(type_spec))
244}
245
246/// Analyzes a MON source string, parsing, resolving, and validating it.
247///
248/// This is the primary entry point for processing MON data. It returns an
249/// [`AnalysisResult`] on success, which contains the fully resolved document
250/// and provides methods for serialization and LSP-related queries.
251///
252/// # Arguments
253///
254/// * `source` - The MON source code as a string.
255/// * `file_name` - The name of the file being analyzed (used for error reporting).
256///
257/// # Errors
258///
259/// Returns a [`MonError`] if parsing, resolution, or validation fails.
260///
261/// # Panics
262///
263/// Panics if the current directory cannot be determined when `file_name` is relative.
264pub fn analyze(source: &str, file_name: &str) -> Result<AnalysisResult, MonError> {
265    let mut parser = Parser::new_with_name(source, file_name.to_string())?;
266    let document = parser.parse_document()?;
267    let unresolved_document = document.clone();
268
269    let mut resolver = Resolver::new();
270    let mut path = PathBuf::from(file_name);
271    if path.is_relative() {
272        path = std::env::current_dir().unwrap().join(path);
273    }
274
275    let resolved_doc = resolver.resolve(document, source, path, None)?;
276
277    Ok(AnalysisResult {
278        document: resolved_doc,
279        unresolved_document,
280        symbol_table: resolver.symbol_table,
281        anchors: resolver.anchors,
282    })
283}
284
285impl Display for TypeSpec {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        match self {
288            TypeSpec::Simple(name, _) => write!(f, "{name}"),
289            TypeSpec::Collection(types, _) => {
290                write!(f, "[")?;
291                for (i, t) in types.iter().enumerate() {
292                    write!(f, "{t}")?;
293                    if i < types.len() - 1 {
294                        write!(f, ", ")?;
295                    }
296                }
297                write!(f, "]")
298            }
299            TypeSpec::Spread(t, _) => write!(f, "{t}..."),
300        }
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use crate::analyze;
307
308    #[test]
309    fn test_simple_parse_to_json() {
310        let source = r#"{
311            name: "My App",
312            version: 1.0,
313            is_enabled: true,
314            features: ["a", "b", "c"],
315            config: {
316                host: "localhost",
317                port: 8080.0,
318            }
319        }"#;
320
321        let expected_json = serde_json::json!({
322            "name": "My App",
323            "version": 1.0,
324            "is_enabled": true,
325            "features": ["a", "b", "c"],
326            "config": {
327                "host": "localhost",
328                "port": 8080.0,
329            }
330        });
331
332        let analysis_result = analyze(source, "test.mon").unwrap();
333        let result = analysis_result.to_json().unwrap();
334        let result_json: serde_json::Value = serde_json::from_str(&result).unwrap();
335
336        assert_eq!(result_json, expected_json);
337    }
338
339    #[test]
340    fn test_analyze_semantic_info() {
341        let source = r#"{
342            MyType: #struct { field(String) },
343            &my_anchor: { a: 1 },
344            value: *my_anchor,
345        }"#;
346
347        let analysis_result = analyze(source, "test.mon").unwrap();
348
349        // Check symbol table
350        assert!(analysis_result.symbol_table.types.contains_key("MyType"));
351
352        // Check anchors
353        assert!(analysis_result.anchors.contains_key("my_anchor"));
354    }
355
356    #[test]
357    fn test_simple_parse_to_yaml() {
358        let source = r#"
359{
360        name: "My App",
361        version: 1.0,
362    is_enabled: true,
363}"#;
364
365        let expected_yaml = "is_enabled: true\nname: My App\nversion: 1.0\n";
366
367        let analysis_result = analyze(source, "test.mon").unwrap();
368        let result = analysis_result.to_yaml().unwrap();
369
370        assert_eq!(result, expected_yaml);
371    }
372}