use std::path::Path;
use std::sync::Arc;
use eure_document::value::ObjectKey;
use eure_schema::SchemaDocument;
use eure_schema::convert::{SchemaSourceMap, document_to_schema_with_layout};
use eure_schema::type_path_trace::LayoutStrategies;
use eure_schema::validate::{ValidationError, validate};
use eure_tree::prelude::Cst;
use eure_tree::tree::InputSpan;
use query_flow::{Db, QueryError, query};
use crate::document::OriginMap;
use crate::report::{
ErrorReport, ErrorReports, Origin, format_error_reports, report_schema_validation_errors,
};
use super::assets::TextFile;
use super::config::ResolveConfig;
use super::error::FileError;
use super::parse::{ParseCst, ParseDocument, ParsedDocument};
#[derive(Clone, PartialEq)]
pub struct ValidatedSchema {
pub schema: Arc<SchemaDocument>,
pub layout: Arc<LayoutStrategies>,
pub source_map: Arc<SchemaSourceMap>,
pub parsed: ParsedDocument,
}
#[derive(Clone, PartialEq)]
pub struct ResolvedSchemaExtension {
pub path: String,
pub origin: Origin,
}
#[derive(Clone, PartialEq)]
pub struct ResolvedSchema {
pub file: TextFile,
pub origin: Option<Origin>,
}
#[query(debug = "{Self}({file})")]
pub fn document_to_schema_query(
db: &impl Db,
file: TextFile,
) -> Result<ValidatedSchema, QueryError> {
let parsed = db.query(ParseDocument::new(file.clone()))?;
let (schema, layout, source_map) =
document_to_schema_with_layout(&parsed.doc).map_err(|kind| FileError {
file: file.clone(),
kind,
})?;
Ok(ValidatedSchema {
schema: Arc::new(schema),
layout: Arc::new(layout),
source_map: Arc::new(source_map),
parsed: parsed.as_ref().clone(),
})
}
#[query(debug = "{Self}({doc_file})")]
pub fn validate_against_schema(
db: &impl Db,
doc_file: TextFile,
) -> Result<ErrorReports, QueryError> {
let Some(schema) = db
.query(ResolveSchema::new(doc_file.clone()))?
.as_ref()
.clone()
else {
return Ok(ErrorReports::new());
};
let doc_result = db.query(ParseDocument::new(doc_file.clone()))?;
let doc_parsed = doc_result.as_ref().clone();
let schema_result = match db.query(DocumentToSchemaQuery::new(schema.file.clone())) {
Ok(result) => result,
Err(QueryError::UserError(e)) => {
if let Some(reports) = e.downcast_ref::<ErrorReports>() {
return Ok(reports.clone());
}
if let Some(origin) = &schema.origin {
return Ok(ErrorReports::from(vec![ErrorReport::error(
format!("Failed to load schema: {}", e),
origin.clone(),
)]));
}
return Err(QueryError::UserError(e));
}
Err(other) => return Err(other),
};
let result = validate(&doc_parsed.doc, &schema_result.schema);
report_schema_validation_errors(db, doc_file, schema.file, &result.errors)
}
#[query(debug = "{Self}({doc_file}, {schema_file})")]
pub fn validate_against_explicit_schema(
db: &impl Db,
doc_file: TextFile,
schema_file: TextFile,
) -> Result<ErrorReports, QueryError> {
let doc_result = db.query(ParseDocument::new(doc_file.clone()))?;
let doc_parsed = doc_result.as_ref().clone();
let schema_result = db.query(DocumentToSchemaQuery::new(schema_file.clone()))?;
let result = validate(&doc_parsed.doc, &schema_result.schema);
report_schema_validation_errors(db, doc_file, schema_file, &result.errors)
}
#[query(debug = "{Self}({doc_file}, {schema_file})")]
pub fn get_validation_errors_formatted_explicit(
db: &impl Db,
doc_file: TextFile,
schema_file: TextFile,
) -> Result<Vec<String>, QueryError> {
let reports = db.query(ValidateAgainstExplicitSchema::new(doc_file, schema_file))?;
let mut formatted = Vec::new();
for report in reports.iter() {
let single_report = ErrorReports::from(vec![report.clone()]);
formatted.push(format_error_reports(db, &single_report, false)?);
}
Ok(formatted)
}
#[query(debug = "{Self}({doc_file})")]
pub fn get_validation_errors_formatted(
db: &impl Db,
doc_file: TextFile,
) -> Result<Vec<String>, QueryError> {
let reports = db.query(ValidateAgainstSchema::new(doc_file))?;
let mut formatted = Vec::new();
for report in reports.iter() {
let single_report = ErrorReports::from(vec![report.clone()]);
formatted.push(format_error_reports(db, &single_report, false)?);
}
Ok(formatted)
}
#[query(debug = "{Self}({file})")]
pub fn get_schema_extension(
db: &impl Db,
file: TextFile,
) -> Result<Option<ResolvedSchemaExtension>, QueryError> {
let parsed = db.query(ParseDocument::new(file.clone()))?;
let root_id = parsed.doc.get_root_id();
let root_ctx = parsed.doc.parse_context(root_id);
let Some(schema_ctx) = root_ctx.ext_optional("schema") else {
return Ok(None);
};
let Ok(Some(schema_path)) = root_ctx.parse_ext_optional::<String>("schema") else {
return Ok(None); };
let node_id = schema_ctx.node_id();
let cst = db.query(ParseCst::new(file.clone()))?;
let span = parsed
.origins
.get_value_span(node_id, &cst.cst)
.unwrap_or(InputSpan::EMPTY);
let origin = Origin::new(file, span);
Ok(Some(ResolvedSchemaExtension {
path: schema_path,
origin,
}))
}
#[query(debug = "{Self}({file})")]
pub fn get_schema_extension_diagnostics(
db: &impl Db,
file: TextFile,
) -> Result<ErrorReports, QueryError> {
let result = db.query(ParseDocument::new(file.clone()))?;
let parsed = result.as_ref().clone();
let root_id = parsed.doc.get_root_id();
let root_ctx = parsed.doc.parse_context(root_id);
let Some(schema_ctx) = root_ctx.ext_optional("schema") else {
return Ok(ErrorReports::new());
};
if root_ctx.parse_ext_optional::<String>("schema").is_ok() {
return Ok(ErrorReports::new());
}
let node_id = schema_ctx.node_id();
let cst = db.query(ParseCst::new(file.clone()))?;
let span = parsed.origins.get_value_span(node_id, &cst.cst);
let origin = crate::report::Origin {
file,
span: span.unwrap_or(eure_tree::tree::InputSpan { start: 0, end: 1 }),
hints: Default::default(),
is_fallback: span.is_none(),
};
Ok(ErrorReports::from(vec![ErrorReport::error(
"$schema must be a string path to a schema file",
origin,
)]))
}
#[query(debug = "{Self}({file})")]
pub fn resolve_schema(db: &impl Db, file: TextFile) -> Result<Option<ResolvedSchema>, QueryError> {
if let Some(ext) = db.query(GetSchemaExtension::new(file.clone()))?.as_ref() {
if let Some(base_path) = file.as_local_path() {
let base_dir = base_path.parent().unwrap_or(Path::new("."));
return Ok(Some(ResolvedSchema {
file: TextFile::resolve(&ext.path, base_dir)?,
origin: Some(ext.origin.clone()),
}));
}
if ext.path.starts_with("https://") {
return Ok(Some(ResolvedSchema {
file: TextFile::parse(&ext.path)?,
origin: Some(ext.origin.clone()),
}));
}
}
if let Some(file_path) = file.as_local_path()
&& let Some(resolved) = db.query(ResolveConfig::new(file.clone()))?.as_ref()
&& let Some(schema_path) = resolved
.config
.schema_for_path(file_path, &resolved.config_dir)
{
return Ok(Some(ResolvedSchema {
file: TextFile::resolve(&schema_path, &resolved.config_dir)?,
origin: None, }));
}
if file.ends_with(".schema.eure") {
return Ok(Some(ResolvedSchema {
file: meta_schema_file(),
origin: None, }));
}
Ok(None)
}
fn meta_schema_file() -> TextFile {
TextFile::parse(concat!(
"https://eure.dev/v",
env!("CARGO_PKG_VERSION"),
"/schemas/eure-schema.schema.eure"
))
.expect("hardcoded meta-schema URL is valid")
}
pub fn resolve_validation_error_span(
error: &ValidationError,
origins: &OriginMap,
cst: &Cst,
) -> Option<InputSpan> {
let (node_id, _schema_node_id) = error.node_ids();
match error {
ValidationError::UnknownField { field, node_id, .. } => {
let key = ObjectKey::String(field.clone());
origins
.get_key_span(*node_id, &key, cst)
.or_else(|| origins.get_value_span(*node_id, cst))
}
ValidationError::InvalidKeyType { key, node_id, .. } => origins
.get_key_span(*node_id, key, cst)
.or_else(|| origins.get_value_span(*node_id, cst)),
ValidationError::MissingRequiredField { .. } => origins.get_value_span(node_id, cst),
_ => origins.get_value_span(node_id, cst),
}
}