use crate::types::SchemaValue;
use crate::vocabulary;
use super::diagnostics::{DiagnosticCode, Severity, ValidationDiagnostic};
const PRIMITIVE_TYPES: &[&str] = &[
"Text",
"Number",
"Boolean",
"Date",
"DateTime",
"Time",
"URL",
"Integer",
"Float",
"CssSelectorType",
"PronounceableText",
"XPathType",
];
pub(crate) fn check_value(
value: &SchemaValue,
prop_name: &str,
expected_types: &[&str],
path: &str,
diagnostics: &mut Vec<ValidationDiagnostic>,
) {
if expected_types.is_empty() {
return;
}
match value {
SchemaValue::Text(s) => check_text_value(s, prop_name, expected_types, path, diagnostics),
SchemaValue::Url(_) => check_url_value(expected_types, path, diagnostics),
SchemaValue::Node(node) => {
check_node_value(&node.types, prop_name, expected_types, path, diagnostics);
}
SchemaValue::Number(_) => check_number_value(expected_types, path, diagnostics),
SchemaValue::Boolean(_) => check_boolean_value(expected_types, path, diagnostics),
SchemaValue::DateTime(_) => check_datetime_value(expected_types, path, diagnostics),
}
}
fn check_text_value(
text: &str,
prop_name: &str,
expected: &[&str],
path: &str,
diagnostics: &mut Vec<ValidationDiagnostic>,
) {
if has_any(
expected,
&["Text", "PronounceableText", "CssSelectorType", "XPathType"],
) {
return;
}
if has_any(expected, &["Date", "DateTime", "Time"]) {
return;
}
if has_any(expected, &["Number", "Integer", "Float"]) {
if text.parse::<f64>().is_err() {
diagnostics.push(ValidationDiagnostic {
path: path.to_string(),
severity: Severity::Warning,
code: DiagnosticCode::InvalidNumber,
message: format!("Property '{prop_name}' expects a number, got text: \"{text}\""),
source_location: None,
});
}
return;
}
if has_any(expected, &["Boolean"]) && (text == "true" || text == "false") {
diagnostics.push(ValidationDiagnostic {
path: path.to_string(),
severity: Severity::Warning,
code: DiagnosticCode::InvalidBoolean,
message: format!(
"Property '{prop_name}' expects a boolean, \
got string \"{text}\". Use a boolean value instead"
),
source_location: None,
});
return;
}
if has_any(expected, &["URL"]) {
if !text.starts_with("http://")
&& !text.starts_with("https://")
&& !text.starts_with("mailto:")
{
diagnostics.push(ValidationDiagnostic {
path: path.to_string(),
severity: Severity::Warning,
code: DiagnosticCode::ExpectedUrlGotText,
message: format!("Property '{prop_name}' expects a URL"),
source_location: None,
});
}
return;
}
let enum_types: Vec<&&str> = expected
.iter()
.filter(|t| !PRIMITIVE_TYPES.contains(t))
.collect();
if !enum_types.is_empty() {
if vocabulary::lookup_enum_member(text).is_some() {
return; }
if vocabulary::lookup_type(text).is_some() {
return; }
let expected_list = enum_types
.iter()
.map(|t| format!("'{t}'"))
.collect::<Vec<_>>()
.join(", ");
diagnostics.push(ValidationDiagnostic {
path: path.to_string(),
severity: Severity::Error,
code: DiagnosticCode::InvalidEnumValue,
message: format!(
"'{text}' is not a valid value for property \
'{prop_name}' (expected {expected_list} member)"
),
source_location: None,
});
return;
}
let expected_list = expected.join(", ");
diagnostics.push(ValidationDiagnostic {
path: path.to_string(),
severity: Severity::Error,
code: DiagnosticCode::InvalidValueType,
message: format!("Property '{prop_name}' expects {expected_list}, got Text"),
source_location: None,
});
}
fn check_url_value(_expected: &[&str], _path: &str, _diagnostics: &mut Vec<ValidationDiagnostic>) {
}
fn check_node_value(
node_types: &[String],
prop_name: &str,
expected: &[&str],
path: &str,
diagnostics: &mut Vec<ValidationDiagnostic>,
) {
let all_primitive = expected.iter().all(|t| PRIMITIVE_TYPES.contains(t));
if all_primitive {
let expected_list = expected.join(", ");
diagnostics.push(ValidationDiagnostic {
path: path.to_string(),
severity: Severity::Error,
code: DiagnosticCode::ExpectedTextGotNode,
message: format!("Property '{prop_name}' expects {expected_list}, not a nested object"),
source_location: None,
});
return;
}
if node_types.is_empty() {
return;
}
let matches = node_types.iter().any(|node_type| {
expected.iter().any(|expected_type| {
if PRIMITIVE_TYPES.contains(expected_type) {
return false;
}
if node_type == expected_type {
return true;
}
is_subtype(node_type, expected_type)
})
});
if !matches {
let node_type_list = node_types.join(", ");
let expected_list = expected
.iter()
.filter(|t| !PRIMITIVE_TYPES.contains(t))
.map(|t| format!("'{t}'"))
.collect::<Vec<_>>()
.join(", ");
diagnostics.push(ValidationDiagnostic {
path: path.to_string(),
severity: Severity::Error,
code: DiagnosticCode::InvalidValueType,
message: format!(
"Property '{prop_name}' expects {expected_list}, got '{node_type_list}'"
),
source_location: None,
});
}
}
fn check_number_value(
_expected: &[&str],
_path: &str,
_diagnostics: &mut Vec<ValidationDiagnostic>,
) {
}
fn check_boolean_value(
_expected: &[&str],
_path: &str,
_diagnostics: &mut Vec<ValidationDiagnostic>,
) {
}
fn check_datetime_value(
_expected: &[&str],
_path: &str,
_diagnostics: &mut Vec<ValidationDiagnostic>,
) {
}
fn has_any(expected: &[&str], candidates: &[&str]) -> bool {
candidates.iter().any(|c| expected.contains(c))
}
fn is_subtype(child_type: &str, ancestor_type: &str) -> bool {
vocabulary::is_subtype(child_type, ancestor_type)
}