use std::sync::Arc;
use referencing::{Draft, Registry, Retrieve};
use serde_json::Value;
use crate::{
canonical::{
context::CanonicalizationContext,
emptiness,
ir::{RawJson, Schema, SchemaKind},
parse,
schema::CanonicalSchema,
CanonicalizationError, DefinitionMap,
},
compiler::{
formats_are_assertions_by_default, normalize_base_uri, resolve_base_uri, validate_schema,
},
options::{PatternEngineOptions, PatternOptions},
};
#[must_use]
pub fn options() -> CanonicalizeOptions<'static> {
CanonicalizeOptions::default()
}
#[derive(Default)]
pub struct CanonicalizeOptions<'r> {
registry: Option<&'r Registry<'r>>,
retriever: Option<Arc<dyn Retrieve>>,
base_uri: Option<String>,
pattern_options: PatternEngineOptions,
draft: Option<Draft>,
validate_formats: Option<bool>,
}
impl<'r> CanonicalizeOptions<'r> {
#[must_use]
pub fn with_registry(mut self, registry: &'r Registry<'r>) -> Self {
self.registry = Some(registry);
self
}
#[must_use]
pub fn with_retriever(mut self, retriever: impl Retrieve + 'static) -> Self {
self.retriever = Some(Arc::new(retriever));
self
}
#[must_use]
pub fn with_base_uri(mut self, base_uri: impl Into<String>) -> Self {
self.base_uri = Some(base_uri.into());
self
}
#[must_use]
pub fn with_draft(mut self, draft: Draft) -> Self {
self.draft = Some(draft);
self
}
#[must_use]
pub fn should_validate_formats(mut self, enabled: bool) -> Self {
self.validate_formats = Some(enabled);
self
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn with_pattern_options<E>(mut self, options: PatternOptions<E>) -> Self {
self.pattern_options = options.inner;
self
}
pub fn canonicalize(self, value: &Value) -> Result<CanonicalSchema, CanonicalizationError> {
build(value, &self)
}
}
fn build(
value: &Value,
options: &CanonicalizeOptions<'_>,
) -> Result<CanonicalSchema, CanonicalizationError> {
match value {
Value::Bool(_) | Value::Object(_) => {}
other @ (Value::Null | Value::Number(_) | Value::String(_) | Value::Array(_)) => {
return Err(CanonicalizationError::InvalidSchemaType(other.to_string()))
}
}
let pattern_options = options.pattern_options;
let draft = detect_draft(value, options.draft, options.registry)?;
if draft == Draft::Unknown {
return Ok(CanonicalSchema::new(
Schema::new(SchemaKind::Raw(RawJson::new(value.clone()))),
draft,
pattern_options,
options.validate_formats.unwrap_or(false),
Arc::new(DefinitionMap::new()),
));
}
let validate_formats = options
.validate_formats
.unwrap_or_else(|| formats_are_assertions_by_default(draft));
validate_schema(draft, value)?;
let resource = draft.create_resource_ref(value);
let base_uri = resolve_base_uri(options.base_uri.as_ref(), resource.id())?;
let mut builder = match options.registry {
Some(registry) => registry.add(base_uri.as_str(), resource)?,
None => Registry::new().add(base_uri.as_str(), resource)?,
};
if let Some(retriever) = &options.retriever {
builder = builder.retriever(Arc::clone(retriever));
}
let registry = builder.draft(draft).prepare()?;
let base_uri = normalize_base_uri(®istry, &base_uri);
let resolver = registry.resolver(base_uri);
let context = CanonicalizationContext::new(draft, pattern_options, validate_formats);
let (inner, definitions) = match parse::parse(value, &context, &resolver)? {
Some(parsed) => {
let parsed = emptiness::fold_definitions(parsed, value, &context, &resolver)?;
(parsed.root, Arc::new(parsed.definitions))
}
None => (
Schema::new(SchemaKind::Raw(RawJson::new(value.clone()))),
Arc::new(DefinitionMap::new()),
),
};
Ok(CanonicalSchema::new(
inner,
draft,
pattern_options,
validate_formats,
definitions,
))
}
fn detect_draft<'r>(
value: &Value,
draft: Option<Draft>,
registry: Option<&'r Registry<'r>>,
) -> Result<Draft, CanonicalizationError> {
let mut options = crate::options();
if let Some(draft) = draft {
options = options.with_draft(draft);
}
if let Some(registry) = registry {
options = options.with_registry(registry);
}
options
.draft_for(value)
.map_err(CanonicalizationError::from)
}