use super::diagnostic::{Diagnostic, DiagnosticCode, Location, Severity};
use crate::preset::PresetRaw;
pub fn check_schema(toml_str: &str) -> Vec<Diagnostic> {
let toml_value: toml::Value = match toml::from_str(toml_str) {
Ok(v) => v,
Err(_) => return vec![], };
let json_value = match toml_to_json(&toml_value) {
Ok(v) => v,
Err(_) => return vec![],
};
let schema = schemars::schema_for!(PresetRaw);
let schema_json =
serde_json::to_value(&schema).expect("schemars schema is always serializable");
let validator = match jsonschema::validator_for(&schema_json) {
Ok(v) => v,
Err(_) => return vec![],
};
let mut diagnostics = Vec::new();
for error in validator.iter_errors(&json_value) {
let (code, message) = classify_error(&error);
let field = error
.instance_path
.as_str()
.trim_start_matches('/')
.replace('/', ".");
let (line, column) = super::structural::find_position_by_path(toml_str, &field);
diagnostics.push(Diagnostic {
severity: Severity::Error,
code,
message,
location: Location {
line,
column,
field,
},
});
}
diagnostics
}
pub fn find_unknown_fields(toml_str: &str) -> Vec<Diagnostic> {
let toml_value: toml::Value = match toml::from_str(toml_str) {
Ok(v) => v,
Err(_) => return vec![],
};
let json_value = match toml_to_json(&toml_value) {
Ok(v) => v,
Err(_) => return vec![],
};
let schema = schemars::schema_for!(PresetRaw);
let schema_json = serde_json::to_value(&schema).expect("schema serializes");
let mut diagnostics = Vec::new();
walk_unknown_fields(
&json_value,
&schema_json,
&schema_json,
"",
toml_str,
&mut diagnostics,
);
diagnostics
}
fn walk_unknown_fields(
value: &serde_json::Value,
schema_node: &serde_json::Value,
schema_root: &serde_json::Value,
path_prefix: &str,
toml_str: &str,
diagnostics: &mut Vec<Diagnostic>,
) {
let object_schema = match resolve_to_object_schema(schema_node, schema_root) {
Some(s) => s,
None => return,
};
let value_obj = match value.as_object() {
Some(o) => o,
None => return,
};
let properties = object_schema.get("properties").and_then(|p| p.as_object());
for (field, field_value) in value_obj.iter() {
let field_path = if path_prefix.is_empty() {
field.clone()
} else {
format!("{}.{}", path_prefix, field)
};
let is_top_level = path_prefix.is_empty();
match properties.and_then(|p| p.get(field)) {
Some(field_schema) => {
walk_unknown_fields(
field_value,
field_schema,
schema_root,
&field_path,
toml_str,
diagnostics,
);
}
None if !is_top_level => {
let parent = path_prefix;
let (line, column) =
super::structural::find_position_by_path(toml_str, &field_path);
let is_table = field_value.is_object();
let kind = if is_table { "table" } else { "field" };
let code = if is_table {
DiagnosticCode::UnknownTable
} else {
DiagnosticCode::UnknownField
};
diagnostics.push(Diagnostic {
severity: Severity::Error,
code,
message: format!("unknown {} `{}` in section `[{}]`", kind, field, parent),
location: Location {
line,
column,
field: field_path,
},
});
}
None => {
}
}
}
}
fn resolve_to_object_schema<'a>(
schema: &'a serde_json::Value,
root: &'a serde_json::Value,
) -> Option<&'a serde_json::Map<String, serde_json::Value>> {
let map = schema.as_object()?;
if let Some(ref_val) = map.get("$ref").and_then(|r| r.as_str()) {
if let Some(def_name) = ref_val.strip_prefix("#/definitions/") {
let target = root.get("definitions").and_then(|d| d.get(def_name))?;
return resolve_to_object_schema(target, root);
}
return None;
}
if let Some(any_of) = map.get("anyOf").and_then(|a| a.as_array()) {
for branch in any_of {
let branch_map = match branch.as_object() {
Some(m) => m,
None => continue,
};
if branch_map.get("type").and_then(|t| t.as_str()) == Some("null") {
continue;
}
return resolve_to_object_schema(branch, root);
}
return None;
}
if let Some(all_of) = map.get("allOf").and_then(|a| a.as_array()) {
for branch in all_of {
if let Some(resolved) = resolve_to_object_schema(branch, root) {
return Some(resolved);
}
}
return None;
}
if map.get("type").and_then(|t| t.as_str()) == Some("object") || map.contains_key("properties")
{
return Some(map);
}
None
}
fn toml_to_json(value: &toml::Value) -> Result<serde_json::Value, ()> {
let json_str = serde_json::to_string(value).map_err(|_| ())?;
serde_json::from_str(&json_str).map_err(|_| ())
}
fn classify_error(error: &jsonschema::ValidationError) -> (DiagnosticCode, String) {
use jsonschema::error::ValidationErrorKind;
match &error.kind {
ValidationErrorKind::Type { .. } => (
DiagnosticCode::TypeMismatch,
format!(
"type mismatch at `{}`: {}",
error.instance_path.as_str(),
error
),
),
ValidationErrorKind::Required { .. } => (
DiagnosticCode::MissingRequired,
format!(
"missing required field at `{}`: {}",
error.instance_path.as_str(),
error
),
),
ValidationErrorKind::Maximum { limit } | ValidationErrorKind::Minimum { limit } => (
DiagnosticCode::OutOfRange,
format!(
"`{}` value {} outside allowed range (limit: {})",
error.instance_path.as_str(),
error.instance,
limit
),
),
ValidationErrorKind::ExclusiveMaximum { limit }
| ValidationErrorKind::ExclusiveMinimum { limit } => (
DiagnosticCode::OutOfRange,
format!(
"`{}` value {} outside allowed range (exclusive limit: {})",
error.instance_path.as_str(),
error.instance,
limit
),
),
_ => (
DiagnosticCode::TypeMismatch,
format!(
"schema violation at `{}`: {}",
error.instance_path.as_str(),
error
),
),
}
}