Skip to main content

lintel_validate/parsers/
toml_parser.rs

1use miette::NamedSource;
2use serde_json::Value;
3
4use crate::diagnostics::ParseDiagnostic;
5
6use super::Parser;
7
8pub struct TomlParser;
9
10impl Parser for TomlParser {
11    fn parse(&self, content: &str, file_name: &str) -> Result<Value, ParseDiagnostic> {
12        let toml_value: toml::Value = toml::from_str(content).map_err(|e| {
13            let offset = e.span().map_or(0, |s| s.start);
14            ParseDiagnostic {
15                src: NamedSource::new(file_name, content.to_string()),
16                span: offset.into(),
17                message: e.message().to_string(),
18            }
19        })?;
20        serde_json::to_value(toml_value).map_err(|e| ParseDiagnostic {
21            src: NamedSource::new(file_name, content.to_string()),
22            span: 0.into(),
23            message: e.to_string(),
24        })
25    }
26
27    fn annotate(&self, content: &str, schema_url: &str) -> Option<String> {
28        Some(format!("# :schema {schema_url}\n{content}"))
29    }
30
31    fn strip_annotation(&self, content: &str) -> String {
32        let mut offset = 0;
33        for line in content.lines() {
34            let trimmed = line.trim();
35            if trimmed.is_empty() {
36                offset += line.len() + 1;
37                continue;
38            }
39            if !trimmed.starts_with('#') {
40                break;
41            }
42            if let Some(after_hash) = trimmed.strip_prefix('#') {
43                let after_hash = after_hash.trim();
44                let is_schema = after_hash
45                    .strip_prefix(":schema")
46                    .is_some_and(|rest| !rest.is_empty())
47                    || after_hash
48                        .strip_prefix("$schema:")
49                        .is_some_and(|rest| !rest.is_empty());
50                if is_schema {
51                    let line_end = offset + line.len();
52                    let remove_end = if content.as_bytes().get(line_end) == Some(&b'\n') {
53                        line_end + 1
54                    } else {
55                        line_end
56                    };
57                    return format!("{}{}", &content[..offset], &content[remove_end..]);
58                }
59            }
60            offset += line.len() + 1;
61        }
62        content.to_string()
63    }
64
65    fn extract_schema_uri(&self, content: &str, _value: &Value) -> Option<String> {
66        for line in content.lines() {
67            let trimmed = line.trim();
68            if trimmed.is_empty() {
69                continue;
70            }
71            if !trimmed.starts_with('#') {
72                break;
73            }
74            let after_hash = trimmed.strip_prefix('#')?.trim();
75            // Taplo / Even Better TOML convention: # :schema URL
76            if let Some(uri) = after_hash.strip_prefix(":schema") {
77                let uri = uri.trim();
78                if !uri.is_empty() {
79                    return Some(uri.to_string());
80                }
81            }
82            // Legacy convention: # $schema: URL
83            if let Some(uri) = after_hash.strip_prefix("$schema:") {
84                let uri = uri.trim();
85                if !uri.is_empty() {
86                    return Some(uri.to_string());
87                }
88            }
89        }
90        None
91    }
92}