mon_core/
api.rs

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