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 use crate::ty::Prim;
175
176 fn expected_api() -> Api {
177 Api {
178 version: CURRENT_SCHEMA_VERSION.to_string(),
179 modules: vec![Module {
180 name: "math".to_string(),
181 doc: None,
182 functions: vec![Function {
183 name: "add".to_string(),
184 params: vec![
185 Param {
186 name: "a".to_string(),
187 ty: TypeRef::Prim(Prim::I32),
188 doc: None,
189 },
190 Param {
191 name: "b".to_string(),
192 ty: TypeRef::Prim(Prim::I32),
193 doc: None,
194 },
195 ],
196 returns: Some(TypeRef::Prim(Prim::I32)),
197 doc: Some("Adds two numbers".to_string()),
198 throws: false,
199 r#async: false,
200 cancellable: false,
201 deprecated: None,
202 }],
203 interfaces: vec![],
204 callback_interfaces: vec![],
205 structs: vec![],
206 enums: vec![],
207 errors: None,
208 modules: vec![],
209 }],
210 }
211 }
212
213 #[test]
214 fn every_format_parses_the_same_document() {
215 let yaml = r#"
216version: "0.11.0"
217modules:
218 - name: math
219 functions:
220 - name: add
221 params:
222 - name: a
223 type: i32
224 - name: b
225 type: i32
226 return: i32
227 doc: "Adds two numbers"
228"#;
229 let json = r#"{
230 "version": "0.11.0",
231 "modules": [{
232 "name": "math",
233 "functions": [{
234 "name": "add",
235 "params": [
236 {"name": "a", "type": "i32"},
237 {"name": "b", "type": "i32"}
238 ],
239 "return": "i32",
240 "doc": "Adds two numbers"
241 }]
242 }]
243 }"#;
244 let toml_str = r#"
245version = "0.11.0"
246
247[[modules]]
248name = "math"
249
250[[modules.functions]]
251name = "add"
252return = "i32"
253doc = "Adds two numbers"
254
255[[modules.functions.params]]
256name = "a"
257type = "i32"
258
259[[modules.functions.params]]
260name = "b"
261type = "i32"
262"#;
263 assert_eq!(parse_api_str(yaml, "yaml").unwrap(), expected_api());
264 assert_eq!(parse_api_str(yaml, "yml").unwrap(), expected_api());
265 assert_eq!(parse_api_str(json, "json").unwrap(), expected_api());
266 assert_eq!(parse_api_str(toml_str, "toml").unwrap(), expected_api());
267 }
268
269 #[test]
270 fn unsupported_format_returns_error() {
271 assert!(matches!(
272 parse_api_str("", "xml"),
273 Err(ParseError::UnsupportedFormat(f)) if f == "xml"
274 ));
275 }
276
277 #[test]
278 fn line_col_to_offset_maps_and_clamps() {
279 let src = "ab\ncd\nef";
280 assert_eq!(line_col_to_offset(src, 1, 1), 0);
281 assert_eq!(line_col_to_offset(src, 2, 2), 4);
282 assert_eq!(line_col_to_offset(src, 0, 0), 0);
283 assert_eq!(line_col_to_offset(src, 99, 1), src.len());
284 assert_eq!(line_col_to_offset(src, 1, 99), 2);
285 }
286
287 #[test]
288 fn parse_errors_carry_spans() {
289 let yaml = "version: \"0.11.0\"\nmodules:\n - name: [oops\n";
290 match parse_api_str(yaml, "yaml").unwrap_err() {
291 ParseError::Yaml { line, span, .. } => {
292 assert!(line > 0);
293 assert!(span.is_some());
294 }
295 other => panic!("expected YAML error, got {other:?}"),
296 }
297 match parse_api_str("{\"version\": }", "json").unwrap_err() {
298 ParseError::Json { line, column, .. } => assert!(line > 0 && column > 0),
299 other => panic!("expected JSON error, got {other:?}"),
300 }
301 match parse_api_str("version = ", "toml").unwrap_err() {
302 ParseError::Toml { span, .. } => assert!(span.is_some()),
303 other => panic!("expected TOML error, got {other:?}"),
304 }
305 }
306
307 #[test]
308 fn unknown_type_syntax_is_a_parse_error() {
309 let yaml = "version: \"0.11.0\"\nmodules:\n - name: m\n functions:\n - name: f\n params: [{ name: x, type: \"{string}\" }]\n";
310 let err = parse_api_str(yaml, "yaml").unwrap_err();
311 assert!(err.to_string().contains("map type missing"), "{err}");
312 }
313}