use std::io::BufRead;
use std::sync::Arc;
use crate::document::XmlDocument;
use crate::error::{Result, StructuredError};
use crate::schema::fetcher::SchemaFetcher;
use crate::schema::types::CompiledSchema;
use super::ValidationMode;
use super::dom::DomSchemaValidator;
use super::streaming::OnePassSchemaValidator;
enum Source<'a> {
Dom(&'a XmlDocument),
Bytes(&'a [u8]),
Reader(Box<dyn BufRead + 'a>),
}
pub struct Validator<'a> {
source: Source<'a>,
schema: Option<Arc<CompiledSchema>>,
mode: ValidationMode,
max_errors: Option<usize>,
}
impl<'a> From<&'a XmlDocument> for Validator<'a> {
fn from(doc: &'a XmlDocument) -> Self {
Self::with_source(Source::Dom(doc))
}
}
impl<'a> From<&'a str> for Validator<'a> {
fn from(xml: &'a str) -> Self {
Self::with_source(Source::Bytes(xml.as_bytes()))
}
}
impl<'a> From<&'a [u8]> for Validator<'a> {
fn from(xml: &'a [u8]) -> Self {
Self::with_source(Source::Bytes(xml))
}
}
impl<'a> Validator<'a> {
fn with_source(source: Source<'a>) -> Self {
Self {
source,
schema: None,
mode: ValidationMode::default(),
max_errors: None,
}
}
pub fn from_reader<R: BufRead + 'a>(reader: R) -> Self {
Self::with_source(Source::Reader(Box::new(reader)))
}
pub fn schema(mut self, schema: impl Into<Arc<CompiledSchema>>) -> Self {
self.schema = Some(schema.into());
self
}
pub fn mode(mut self, mode: ValidationMode) -> Self {
self.mode = mode;
self
}
pub fn max_errors(mut self, max: usize) -> Self {
self.max_errors = Some(max);
self
}
pub fn run(self) -> Result<Report> {
let Self {
source,
schema,
mode,
max_errors,
} = self;
let entries = match schema {
Some(schema) => validate_with_schema(source, schema, mode, max_errors)?,
None => run_location_default(source)?,
};
Ok(Report::new(entries))
}
pub fn run_with<F: SchemaFetcher + 'static>(self, fetcher: F) -> Result<Report> {
let Self {
source,
schema,
mode,
max_errors,
} = self;
let entries = match schema {
Some(schema) => validate_with_schema(source, schema, mode, max_errors)?,
None => match source {
Source::Dom(doc) => {
super::api::validate_with_schema_location_and_fetcher(doc, &fetcher)?
}
Source::Bytes(bytes) => {
super::api::streaming_validate_with_schema_location_and_fetcher(bytes, fetcher)?
}
Source::Reader(reader) => {
super::api::streaming_validate_with_schema_location_and_fetcher(
reader, fetcher,
)?
}
},
};
Ok(Report::new(entries))
}
#[cfg(feature = "tokio")]
pub async fn run_async_with<F: crate::schema::fetcher::AsyncSchemaFetcher>(
self,
fetcher: &F,
) -> Result<Report> {
let Self {
source,
schema,
mode,
max_errors,
} = self;
let entries = match schema {
Some(schema) => validate_with_schema(source, schema, mode, max_errors)?,
None => match source {
Source::Dom(doc) => {
super::api::validate_with_schema_location_with_async_fetcher(doc, fetcher)
.await?
}
_ => {
return Err(crate::error::Error::InvalidOperation(
"async xsi:schemaLocation resolution is only supported for DOM input; \
use run_with for streaming input, or provide a schema"
.to_string(),
));
}
},
};
Ok(Report::new(entries))
}
#[cfg(feature = "tokio")]
pub async fn run_async(self) -> Result<Report> {
let fetcher = crate::schema::fetcher::AsyncDefaultFetcher::new()?;
self.run_async_with(&fetcher).await
}
}
fn validate_with_schema(
source: Source<'_>,
schema: Arc<CompiledSchema>,
mode: ValidationMode,
max_errors: Option<usize>,
) -> Result<Vec<StructuredError>> {
match source {
Source::Dom(doc) => {
let mut validator = DomSchemaValidator::new(schema).with_mode(mode);
if let Some(max) = max_errors {
validator = validator.with_max_errors(max);
}
validator.validate(doc)
}
Source::Bytes(bytes) => run_streaming_with_schema(bytes, schema, mode, max_errors),
Source::Reader(reader) => run_streaming_with_schema(reader, schema, mode, max_errors),
}
}
fn run_streaming_with_schema<R: BufRead>(
reader: R,
schema: Arc<CompiledSchema>,
mode: ValidationMode,
max_errors: Option<usize>,
) -> Result<Vec<StructuredError>> {
let mut validator = OnePassSchemaValidator::new(schema).set_mode(mode);
if let Some(max) = max_errors {
validator = validator.with_max_errors(max);
}
validator.validate(reader)
}
#[cfg(feature = "ureq")]
fn run_location_default(source: Source<'_>) -> Result<Vec<StructuredError>> {
match source {
Source::Dom(doc) => super::api::validate_with_schema_location(doc),
Source::Bytes(bytes) => super::api::streaming_validate_with_schema_location(bytes),
Source::Reader(reader) => super::api::streaming_validate_with_schema_location(reader),
}
}
#[cfg(not(feature = "ureq"))]
fn run_location_default(_source: Source<'_>) -> Result<Vec<StructuredError>> {
Err(crate::error::Error::InvalidOperation(
"resolving xsi:schemaLocation with the default fetcher requires the `ureq` feature; \
enable it, call .schema(...) with an explicit schema, or use .run_with(fetcher)"
.to_string(),
))
}
#[derive(Debug, Clone)]
pub struct Report {
entries: Vec<StructuredError>,
}
impl Report {
fn new(entries: Vec<StructuredError>) -> Self {
Self { entries }
}
pub fn is_valid(&self) -> bool {
!self.entries.iter().any(|e| e.is_error())
}
pub fn is_clean(&self) -> bool {
self.entries.is_empty()
}
pub fn entries(&self) -> &[StructuredError] {
&self.entries
}
pub fn errors(&self) -> Vec<&StructuredError> {
self.entries.iter().filter(|e| e.is_error()).collect()
}
pub fn warnings(&self) -> Vec<&StructuredError> {
self.entries.iter().filter(|e| e.is_warning()).collect()
}
pub fn error_count(&self) -> usize {
self.entries.iter().filter(|e| e.is_error()).count()
}
pub fn warning_count(&self) -> usize {
self.entries.iter().filter(|e| e.is_warning()).count()
}
pub fn into_entries(self) -> Vec<StructuredError> {
self.entries
}
}