schema_index_yaml/
parser.rs1use schema_core::ParseFrom;
2
3use crate::SUPPORTED_VERSIONS;
4
5#[derive(thiserror::Error, Debug)]
6pub enum ParseError {
7 #[error("{0}")]
10 Syntax(String),
11 #[error("unsupported schema version {got}; supported versions: {supported}")]
12 UnsupportedVersion { got: u8, supported: &'static str },
13}
14
15impl<T: AsRef<str>> ParseFrom<T> for super::SchemaYaml {
16 type Error = ParseError;
17
18 fn try_parse(value: T) -> Result<Self, Self::Error> {
19 let source = value.as_ref();
20 let result: super::SchemaYaml = serde_yaml::from_str(source)
21 .map_err(|error| ParseError::Syntax(render_yaml_error(source, &error)))?;
22
23 if !SUPPORTED_VERSIONS.contains(&result.version) {
24 return Err(ParseError::UnsupportedVersion {
25 got: result.version,
26 supported: "1",
27 });
28 }
29
30 Ok(result)
31 }
32}
33
34fn render_yaml_error(source: &str, error: &serde_yaml::Error) -> String {
47 let message = clean_message(&error.to_string());
48 match error.location() {
49 Some(location) if !is_field_scoped(&message) => {
50 render_snippet(source, location.line(), location.column(), &message)
51 }
52 _ => message,
53 }
54}
55
56fn is_field_scoped(message: &str) -> bool {
60 message.starts_with('`') || message.starts_with("field ")
61}
62
63fn clean_message(raw: &str) -> String {
71 let without_location = match raw.rfind(" at line ") {
72 Some(idx) => raw.get(..idx).unwrap_or(raw),
73 None => raw,
74 };
75
76 let mut trimmed = without_location;
79 while let Some(rest) = trimmed.strip_prefix("fields: ") {
80 trimmed = rest;
81 }
82
83 trimmed
84 .replace("`field`, ", "")
85 .replace(", `field`", "")
86 .replace("unknown field", "unknown key")
87 .replace("missing field", "missing key")
88}
89
90fn render_snippet(source: &str, line: usize, column: usize, message: &str) -> String {
93 let text = line.checked_sub(1).and_then(|idx| source.lines().nth(idx));
94 let Some(text) = text else {
95 return format!("{message} (line {line}, column {column})");
96 };
97
98 let number = line.to_string();
99 let gutter = " ".repeat(number.len());
100 let caret_indent = " ".repeat(column.saturating_sub(1));
101 format!(
102 "{message}\n{gutter}--> line {line}, column {column}\n\
103 {gutter} |\n\
104 {number} | {text}\n\
105 {gutter} | {caret_indent}^"
106 )
107}