use crate::error::{SchemaError, SchemaResult};
use crate::ids::DocumentId;
use crate::parser::parse::parse_schema_with_config;
use crate::parser::resolver::{
fixup_composition_edges, resolve_all_directives, ResolverConfig, SchemaLoader, SchemaResolver,
};
#[cfg(feature = "async")]
use crate::parser::resolver::{resolve_all_directives_async, AsyncSchemaLoader};
use crate::pipeline::process_loaded_schemas;
use crate::schema::model::{RegexCompat, XsdVersion};
use crate::schema::SchemaSet;
use std::path::{Path, PathBuf};
pub struct SchemaSetBuilder {
schema_set: SchemaSet,
resolver: SchemaResolver,
pending_docs: Vec<DocumentId>,
errors: Vec<SchemaError>,
import_errors: Vec<SchemaError>,
}
impl SchemaSetBuilder {
pub fn new() -> Self {
let mut resolver = SchemaResolver::new();
resolver.catalog_mut().add_xml_catalog();
Self {
schema_set: SchemaSet::new(),
resolver,
pending_docs: Vec::new(),
errors: Vec::new(),
import_errors: Vec::new(),
}
}
pub fn with_config(config: ResolverConfig) -> Self {
let mut resolver = SchemaResolver::with_config(config);
resolver.catalog_mut().add_xml_catalog();
Self {
schema_set: SchemaSet::new(),
resolver,
pending_docs: Vec::new(),
errors: Vec::new(),
import_errors: Vec::new(),
}
}
pub fn with_loader(loader: Box<dyn SchemaLoader>) -> Self {
let mut resolver = SchemaResolver::with_loader(loader);
resolver.catalog_mut().add_xml_catalog();
Self {
schema_set: SchemaSet::new(),
resolver,
pending_docs: Vec::new(),
errors: Vec::new(),
import_errors: Vec::new(),
}
}
pub fn with_version(version: XsdVersion) -> Self {
let mut resolver = SchemaResolver::new();
resolver.catalog_mut().add_xml_catalog();
Self {
schema_set: SchemaSet::with_version(version),
resolver,
pending_docs: Vec::new(),
errors: Vec::new(),
import_errors: Vec::new(),
}
}
pub fn xsd11() -> Self {
Self::with_version(XsdVersion::V1_1)
}
pub fn set_regex_compatibility(&mut self, compat: RegexCompat) -> &mut Self {
self.schema_set.set_regex_compatibility(compat);
self
}
pub fn xsd11_with_loader(loader: Box<dyn SchemaLoader>) -> Self {
let mut resolver = SchemaResolver::with_loader(loader);
resolver.catalog_mut().add_xml_catalog();
Self {
schema_set: SchemaSet::with_version(XsdVersion::V1_1),
resolver,
pending_docs: Vec::new(),
errors: Vec::new(),
import_errors: Vec::new(),
}
}
#[cfg(feature = "async")]
pub fn with_async_loader(loader: Box<dyn AsyncSchemaLoader>) -> Self {
let mut resolver = SchemaResolver::with_async_loader(loader);
resolver.catalog_mut().add_xml_catalog();
Self {
schema_set: SchemaSet::new(),
resolver,
pending_docs: Vec::new(),
errors: Vec::new(),
import_errors: Vec::new(),
}
}
pub fn add_from(&mut self, schema_set: &SchemaSet) -> &mut Self {
for location in schema_set.loaded_schema_locations() {
let _ = self.try_add(location);
}
self
}
pub fn add(mut self, _namespace: &str, location: &str) -> SchemaResult<Self> {
self.try_add(location)?;
Ok(self)
}
pub fn try_add(&mut self, location: &str) -> SchemaResult<bool> {
let normalized = normalize_loaded_location(&self.resolver, location, "");
if self.schema_set.is_loaded(&normalized) {
return Ok(false);
}
let content = self.resolver.load_content(&normalized)?;
let doc_id = parse_schema_with_config(
content.as_bytes(),
&normalized,
&mut self.schema_set,
&self.resolver.config.parser_config,
)?;
self.pending_docs.push(doc_id);
self.schema_set.mark_loaded(normalized, doc_id);
Ok(true)
}
pub fn try_add_relative(&mut self, location: &str, base_uri: &str) -> SchemaResult<bool> {
let normalized = normalize_loaded_location(&self.resolver, location, base_uri);
self.try_add(&normalized)
}
pub fn add_source(mut self, xml: &str, base_uri: &str) -> SchemaResult<Self> {
let normalized = normalize_loaded_location(&self.resolver, base_uri, "");
let doc_id = parse_schema_with_config(
xml.as_bytes(),
&normalized,
&mut self.schema_set,
&self.resolver.config.parser_config,
)?;
self.pending_docs.push(doc_id);
self.schema_set.mark_loaded(normalized, doc_id);
Ok(self)
}
pub fn add_bytes(mut self, xml: &[u8], base_uri: &str) -> SchemaResult<Self> {
let normalized = normalize_loaded_location(&self.resolver, base_uri, "");
let doc_id = parse_schema_with_config(
xml,
&normalized,
&mut self.schema_set,
&self.resolver.config.parser_config,
)?;
self.pending_docs.push(doc_id);
self.schema_set.mark_loaded(normalized, doc_id);
Ok(self)
}
pub fn schema_count(&self) -> usize {
self.pending_docs.len()
}
pub fn is_loaded(&self, location: &str) -> bool {
self.schema_set.is_loaded(location)
}
pub fn compile(mut self) -> SchemaResult<CompiledSchemaSet> {
let pending: Vec<_> = self.pending_docs.drain(..).collect();
for doc_id in pending {
self.resolve_directives_recursive(doc_id)?;
}
fixup_composition_edges(&mut self.schema_set);
if let Some(err) = self
.errors
.into_iter()
.chain(self.import_errors)
.find(|e| e.is_schema_content_error())
{
return Err(err);
}
let (inline_stats, resolution_stats) = process_loaded_schemas(&mut self.schema_set)?;
let documents_loaded = self.schema_set.documents.len();
Ok(CompiledSchemaSet {
schema_set: self.schema_set,
stats: CompilationStats {
documents_loaded,
inline_types_assembled: inline_stats.total_inline_types,
types_resolved: resolution_stats.types_resolved,
elements_resolved: resolution_stats.elements_resolved,
attributes_resolved: resolution_stats.attributes_resolved,
groups_resolved: resolution_stats.groups_resolved,
attribute_groups_resolved: resolution_stats.attribute_groups_resolved,
},
})
}
fn resolve_directives_recursive(&mut self, doc_id: DocumentId) -> SchemaResult<()> {
let result = resolve_all_directives(doc_id, &mut self.resolver, &mut self.schema_set);
for loaded_id in result.loaded {
self.resolve_directives_recursive(loaded_id)?;
}
self.errors.extend(result.errors);
self.import_errors.extend(result.import_errors);
Ok(())
}
#[cfg(feature = "async")]
async fn resolve_directives_recursive_async(&mut self, doc_id: DocumentId) -> SchemaResult<()> {
let result =
resolve_all_directives_async(doc_id, &mut self.resolver, &mut self.schema_set).await;
for loaded_id in result.loaded {
Box::pin(self.resolve_directives_recursive_async(loaded_id)).await?;
}
self.errors.extend(result.errors);
self.import_errors.extend(result.import_errors);
Ok(())
}
#[cfg(feature = "async")]
pub async fn add_async(mut self, _namespace: &str, location: &str) -> SchemaResult<Self> {
let content = self.resolver.load_content_async(location).await?;
let doc_id = parse_schema_with_config(
content.as_bytes(),
location,
&mut self.schema_set,
&self.resolver.config.parser_config,
)?;
self.pending_docs.push(doc_id);
self.schema_set.mark_loaded(location.to_string(), doc_id);
Ok(self)
}
#[cfg(feature = "async")]
pub async fn compile_async(mut self) -> SchemaResult<CompiledSchemaSet> {
let pending: Vec<_> = self.pending_docs.drain(..).collect();
for doc_id in pending {
self.resolve_directives_recursive_async(doc_id).await?;
}
fixup_composition_edges(&mut self.schema_set);
if let Some(err) = self
.errors
.into_iter()
.chain(self.import_errors)
.find(|e| e.is_schema_content_error())
{
return Err(err);
}
let (inline_stats, resolution_stats) = process_loaded_schemas(&mut self.schema_set)?;
let documents_loaded = self.schema_set.documents.len();
Ok(CompiledSchemaSet {
schema_set: self.schema_set,
stats: CompilationStats {
documents_loaded,
inline_types_assembled: inline_stats.total_inline_types,
types_resolved: resolution_stats.types_resolved,
elements_resolved: resolution_stats.elements_resolved,
attributes_resolved: resolution_stats.attributes_resolved,
groups_resolved: resolution_stats.groups_resolved,
attribute_groups_resolved: resolution_stats.attribute_groups_resolved,
},
})
}
}
fn normalize_loaded_location(resolver: &SchemaResolver, location: &str, base_uri: &str) -> String {
let resolved = resolver
.resolve_location(location, base_uri)
.unwrap_or_else(|_| location.to_string());
if is_absolute_location(&resolved) {
return resolved;
}
let cwd = match std::env::current_dir() {
Ok(cwd) => cwd,
Err(_) => return resolved,
};
normalize_path(&cwd.join(&resolved))
.to_string_lossy()
.into_owned()
}
fn is_absolute_location(location: &str) -> bool {
location.starts_with("http://")
|| location.starts_with("https://")
|| location.starts_with("file://")
|| Path::new(location).is_absolute()
|| (location.len() >= 2 && location.as_bytes().get(1) == Some(&b':'))
}
fn normalize_path(path: &Path) -> PathBuf {
let mut result = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
result.pop();
}
std::path::Component::CurDir => {}
_ => result.push(component),
}
}
result
}
impl Default for SchemaSetBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct CompiledSchemaSet {
pub schema_set: SchemaSet,
pub stats: CompilationStats,
}
impl CompiledSchemaSet {
pub fn schema_set(&self) -> &SchemaSet {
&self.schema_set
}
pub fn into_schema_set(self) -> SchemaSet {
self.schema_set
}
}
#[derive(Debug, Default, Clone)]
pub struct CompilationStats {
pub documents_loaded: usize,
pub inline_types_assembled: usize,
pub types_resolved: usize,
pub elements_resolved: usize,
pub attributes_resolved: usize,
pub groups_resolved: usize,
pub attribute_groups_resolved: usize,
}
impl CompilationStats {
pub fn total_references_resolved(&self) -> usize {
self.types_resolved
+ self.elements_resolved
+ self.attributes_resolved
+ self.groups_resolved
+ self.attribute_groups_resolved
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_new() {
let builder = SchemaSetBuilder::new();
assert_eq!(builder.schema_count(), 0);
}
#[test]
fn test_builder_add_source() {
let xsd = r#"<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="root" type="xs:string"/>
</xs:schema>"#;
let builder = SchemaSetBuilder::new()
.add_source(xsd, "test.xsd")
.expect("Should parse schema");
assert_eq!(builder.schema_count(), 1);
}
#[test]
fn test_builder_compile() {
let xsd = r#"<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="person">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>"#;
let compiled = SchemaSetBuilder::new()
.add_source(xsd, "test.xsd")
.expect("Should parse schema")
.compile()
.expect("Should compile");
assert_eq!(compiled.stats.documents_loaded, 1);
assert!(compiled.stats.inline_types_assembled > 0);
}
#[test]
fn test_builder_multiple_schemas() {
let xsd1 = r#"<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/schema1">
<xs:element name="item1" type="xs:string"/>
</xs:schema>"#;
let xsd2 = r#"<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/schema2">
<xs:element name="item2" type="xs:int"/>
</xs:schema>"#;
let compiled = SchemaSetBuilder::new()
.add_source(xsd1, "schema1.xsd")
.expect("Should parse schema1")
.add_source(xsd2, "schema2.xsd")
.expect("Should parse schema2")
.compile()
.expect("Should compile");
assert_eq!(compiled.stats.documents_loaded, 2);
}
#[test]
fn test_compilation_stats() {
let stats = CompilationStats {
documents_loaded: 2,
inline_types_assembled: 5,
types_resolved: 10,
elements_resolved: 8,
attributes_resolved: 3,
groups_resolved: 2,
attribute_groups_resolved: 1,
};
assert_eq!(stats.total_references_resolved(), 24);
}
}