1use crate::ir::Api;
9use miette::{Diagnostic, SourceSpan};
10
11#[derive(Debug, thiserror::Error, Diagnostic)]
17pub enum ParseError {
18 #[error("unsupported format: {0}")]
21 #[diagnostic(help("supported formats are 'yaml', 'yml', 'json', and 'toml'"))]
22 UnsupportedFormat(String),
23 #[error("YAML parse error at line {line}, column {column}: {message}")]
25 #[diagnostic(help(
26 "check YAML indentation, quoting, and that all required fields have valid values"
27 ))]
28 Yaml {
29 line: usize,
31 column: usize,
33 message: String,
35 #[source_code]
37 src: String,
38 #[label("here")]
40 span: Option<SourceSpan>,
41 },
42 #[error("TOML parse error: {message}")]
44 #[diagnostic(help(
45 "check TOML syntax: keys, table headers, and that values use the correct types"
46 ))]
47 Toml {
48 message: String,
50 #[source_code]
52 src: String,
53 #[label("here")]
55 span: Option<SourceSpan>,
56 },
57 #[error("JSON parse error at line {line}, column {column}: {message}")]
59 #[diagnostic(help(
60 "check JSON syntax: matching braces/brackets, quoted keys, and trailing commas"
61 ))]
62 Json {
63 line: usize,
65 column: usize,
67 message: String,
69 #[source_code]
71 src: String,
72 #[label("here")]
74 span: Option<SourceSpan>,
75 },
76}
77
78pub fn line_col_to_offset(src: &str, line: usize, col: usize) -> usize {
82 if line == 0 {
83 return 0;
84 }
85 let line_idx = line - 1;
86 let col_idx = col.saturating_sub(1);
87
88 let mut start = 0usize;
89 for (i, l) in src.split('\n').enumerate() {
90 if i == line_idx {
91 let line_offset = l
92 .char_indices()
93 .nth(col_idx)
94 .map(|(b, _)| b)
95 .unwrap_or(l.len());
96 return (start + line_offset).min(src.len());
97 }
98 start += l.len() + 1;
99 }
100 src.len()
101}
102
103pub fn parse_api_str(s: &str, format: &str) -> Result<Api, ParseError> {
117 match format {
118 "yaml" | "yml" => serde_yaml::from_str(s).map_err(|e| {
119 let (line, column) = e
120 .location()
121 .map(|m| (m.line(), m.column()))
122 .unwrap_or((0, 0));
123 let span = if line > 0 && column > 0 {
124 Some(SourceSpan::new(
125 line_col_to_offset(s, line, column).into(),
126 1,
127 ))
128 } else {
129 None
130 };
131 ParseError::Yaml {
132 line,
133 column,
134 message: e.to_string(),
135 src: s.to_string(),
136 span,
137 }
138 }),
139 "json" => serde_json::from_str(s).map_err(|e| {
140 let line = e.line();
141 let column = e.column();
142 let span = if line > 0 && column > 0 {
143 Some(SourceSpan::new(
144 line_col_to_offset(s, line, column).into(),
145 1,
146 ))
147 } else {
148 None
149 };
150 ParseError::Json {
151 line,
152 column,
153 message: e.to_string(),
154 src: s.to_string(),
155 span,
156 }
157 }),
158 "toml" => toml::from_str(s).map_err(|e| {
159 let span = e.span().map(|r| SourceSpan::new(r.start.into(), r.len()));
160 ParseError::Toml {
161 message: e.to_string(),
162 src: s.to_string(),
163 span,
164 }
165 }),
166 other => Err(ParseError::UnsupportedFormat(other.to_string())),
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::ir::{Function, Module, Param, TypeRef, CURRENT_SCHEMA_VERSION};
174
175 fn expected_api() -> Api {
176 Api {
177 version: CURRENT_SCHEMA_VERSION.to_string(),
178 modules: vec![Module {
179 name: "math".to_string(),
180 doc: None,
181 functions: vec![Function {
182 name: "add".to_string(),
183 params: vec![
184 Param {
185 name: "a".to_string(),
186 ty: TypeRef::I32,
187 doc: None,
188 },
189 Param {
190 name: "b".to_string(),
191 ty: TypeRef::I32,
192 doc: None,
193 },
194 ],
195 returns: Some(TypeRef::I32),
196 doc: Some("Adds two numbers".to_string()),
197 throws: false,
198 r#async: false,
199 cancellable: false,
200 deprecated: None,
201 }],
202 interfaces: vec![],
203 callback_interfaces: vec![],
204 structs: vec![],
205 enums: vec![],
206 errors: None,
207 modules: vec![],
208 }],
209 }
210 }
211
212 #[test]
213 fn every_format_parses_the_same_document() {
214 let yaml = r#"
215version: "0.10.0"
216modules:
217 - name: math
218 functions:
219 - name: add
220 params:
221 - name: a
222 type: i32
223 - name: b
224 type: i32
225 return: i32
226 doc: "Adds two numbers"
227"#;
228 let json = r#"{
229 "version": "0.10.0",
230 "modules": [{
231 "name": "math",
232 "functions": [{
233 "name": "add",
234 "params": [
235 {"name": "a", "type": "i32"},
236 {"name": "b", "type": "i32"}
237 ],
238 "return": "i32",
239 "doc": "Adds two numbers"
240 }]
241 }]
242 }"#;
243 let toml_str = r#"
244version = "0.10.0"
245
246[[modules]]
247name = "math"
248
249[[modules.functions]]
250name = "add"
251return = "i32"
252doc = "Adds two numbers"
253
254[[modules.functions.params]]
255name = "a"
256type = "i32"
257
258[[modules.functions.params]]
259name = "b"
260type = "i32"
261"#;
262 assert_eq!(parse_api_str(yaml, "yaml").unwrap(), expected_api());
263 assert_eq!(parse_api_str(yaml, "yml").unwrap(), expected_api());
264 assert_eq!(parse_api_str(json, "json").unwrap(), expected_api());
265 assert_eq!(parse_api_str(toml_str, "toml").unwrap(), expected_api());
266 }
267
268 #[test]
269 fn unsupported_format_returns_error() {
270 assert!(matches!(
271 parse_api_str("", "xml"),
272 Err(ParseError::UnsupportedFormat(f)) if f == "xml"
273 ));
274 }
275
276 #[test]
277 fn line_col_to_offset_maps_and_clamps() {
278 let src = "ab\ncd\nef";
279 assert_eq!(line_col_to_offset(src, 1, 1), 0);
280 assert_eq!(line_col_to_offset(src, 2, 2), 4);
281 assert_eq!(line_col_to_offset(src, 0, 0), 0);
282 assert_eq!(line_col_to_offset(src, 99, 1), src.len());
283 assert_eq!(line_col_to_offset(src, 1, 99), 2);
284 }
285
286 #[test]
287 fn parse_errors_carry_spans() {
288 let yaml = "version: \"0.10.0\"\nmodules:\n - name: [oops\n";
289 match parse_api_str(yaml, "yaml").unwrap_err() {
290 ParseError::Yaml { line, span, .. } => {
291 assert!(line > 0);
292 assert!(span.is_some());
293 }
294 other => panic!("expected YAML error, got {other:?}"),
295 }
296 match parse_api_str("{\"version\": }", "json").unwrap_err() {
297 ParseError::Json { line, column, .. } => assert!(line > 0 && column > 0),
298 other => panic!("expected JSON error, got {other:?}"),
299 }
300 match parse_api_str("version = ", "toml").unwrap_err() {
301 ParseError::Toml { span, .. } => assert!(span.is_some()),
302 other => panic!("expected TOML error, got {other:?}"),
303 }
304 }
305
306 #[test]
307 fn unknown_type_syntax_is_a_parse_error() {
308 let yaml = "version: \"0.10.0\"\nmodules:\n - name: m\n functions:\n - name: f\n params: [{ name: x, type: \"{string}\" }]\n";
309 let err = parse_api_str(yaml, "yaml").unwrap_err();
310 assert!(err.to_string().contains("map type missing"), "{err}");
311 }
312}