use std::{
collections::{BTreeSet, HashSet},
sync::Arc,
};
use referencing::{Draft, Registry, Retrieve, Uri};
use serde_json::Value;
use crate::{
canonical::{
context::CanonicalizationContext,
emptiness,
ir::{RawJson, Schema, SchemaKind},
parse, refold,
schema::CanonicalSchema,
CanonicalizationError, DefinitionMap, ROOT_DEFINITION_KEY,
},
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 offline(mut self) -> Self {
self.retriever = Some(Arc::new(crate::retriever::OfflineRetriever));
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> {
self.prepare(value)?.canonicalize()
}
pub fn prepare<'a>(
self,
value: &'a Value,
) -> Result<PreparedDocument<'a>, CanonicalizationError>
where
'r: 'a,
{
prepare(value, &self)
}
}
pub struct PreparedDocument<'a> {
document: &'a Value,
draft: Draft,
pattern_options: PatternEngineOptions,
validate_formats: bool,
resolution: Option<(Registry<'a>, Uri<String>)>,
}
impl PreparedDocument<'_> {
#[must_use]
pub fn draft(&self) -> Draft {
self.draft
}
pub fn canonicalize(&self) -> Result<CanonicalSchema, CanonicalizationError> {
self.reduce(self.document)
}
pub fn canonicalize_at(&self, pointer: &str) -> Result<CanonicalSchema, CanonicalizationError> {
let target = referencing::pointer(self.document, pointer)
.ok_or_else(|| CanonicalizationError::PointerNotFound(pointer.to_string()))?;
match target {
Value::Bool(_) | Value::Object(_) => self.reduce(target),
other @ (Value::Null | Value::Number(_) | Value::String(_) | Value::Array(_)) => {
Err(CanonicalizationError::InvalidSchemaType(other.to_string()))
}
}
}
pub fn unsatisfiable_pointers(&self) -> Result<HashSet<String>, CanonicalizationError> {
let Some((registry, base_uri)) = &self.resolution else {
return Ok(HashSet::new());
};
let resolver = registry.resolver(base_uri.clone());
let context =
CanonicalizationContext::new(self.draft, self.pattern_options, self.validate_formats);
let Some(parsed) = parse::parse_tracking_nodes(self.document, &context, &resolver)? else {
return Ok(HashSet::new());
};
let mut pointers = HashSet::new();
collect_unsatisfiable_pointers(self.document, &mut String::new(), &parsed, &mut pointers);
Ok(pointers)
}
fn reduce(&self, target: &Value) -> Result<CanonicalSchema, CanonicalizationError> {
let opaque = |target: &Value| {
CanonicalSchema::new(
Schema::new(SchemaKind::Raw(RawJson::new(target.clone()))),
self.draft,
self.pattern_options,
self.validate_formats,
Arc::new(DefinitionMap::new()),
Arc::new(BTreeSet::new()),
)
};
let Some((registry, base_uri)) = &self.resolution else {
return Ok(opaque(target));
};
let resolver = registry.resolver(base_uri.clone());
let context =
CanonicalizationContext::new(self.draft, self.pattern_options, self.validate_formats);
let (inner, definitions, local) = match parse::parse(target, &context, &resolver)? {
Some(parsed) => {
let parsed = emptiness::fold_definitions(parsed, target, &context, &resolver)?;
let parsed = refold::through_targets(parsed, &context);
(
parsed.root,
Arc::new(parsed.definitions),
Arc::new(parsed.local_definitions),
)
}
None => return Ok(opaque(target)),
};
Ok(CanonicalSchema::new(
inner,
self.draft,
self.pattern_options,
self.validate_formats,
definitions,
local,
))
}
}
fn prepare<'a, 'r: 'a>(
value: &'a Value,
options: &CanonicalizeOptions<'r>,
) -> Result<PreparedDocument<'a>, 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(PreparedDocument {
document: value,
draft,
pattern_options,
validate_formats: options.validate_formats.unwrap_or(false),
resolution: None,
});
}
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);
Ok(PreparedDocument {
document: value,
draft,
pattern_options,
validate_formats,
resolution: Some((registry, base_uri)),
})
}
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)
}
fn names_unsatisfiable_body(parsed: &parse::ParseOutput, key: &str) -> bool {
let mut key = key;
let mut walked: Vec<&str> = Vec::new();
loop {
if walked.contains(&key) {
return false;
}
walked.push(key);
match parsed.parsed_definitions.get(key) {
Some(parse::ParsedNode::Unsatisfiable) => return true,
Some(parse::ParsedNode::Reference(next)) => {
key = next.as_ref();
continue;
}
None => {}
}
let body = if key == ROOT_DEFINITION_KEY {
&parsed.root
} else {
match parsed.definitions.get(key) {
Some(body) => body,
None => return false,
}
};
match body.kind() {
SchemaKind::False => return true,
SchemaKind::Reference(next) => key = next.as_ref(),
SchemaKind::MultiType(_)
| SchemaKind::TypedGroup { .. }
| SchemaKind::String(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Array(_)
| SchemaKind::Object(_)
| SchemaKind::Const(_)
| SchemaKind::Enum(_)
| SchemaKind::Not(_)
| SchemaKind::AllOf(_)
| SchemaKind::AnyOf(_)
| SchemaKind::OneOf(_)
| SchemaKind::True
| SchemaKind::Raw(_) => return false,
}
}
}
fn collect_unsatisfiable_pointers(
value: &Value,
pointer: &mut String,
parsed: &parse::ParseOutput,
out: &mut HashSet<String>,
) {
let empty = match parsed.parsed_nodes.get(&std::ptr::from_ref(value)) {
Some(parse::ParsedNode::Unsatisfiable) => true,
Some(parse::ParsedNode::Reference(key)) => names_unsatisfiable_body(parsed, key),
None => false,
};
if empty {
out.insert(pointer.clone());
}
let restore = pointer.len();
match value {
Value::Object(map) => {
for (key, child) in map {
pointer.push('/');
referencing::write_escaped_str(pointer, key);
collect_unsatisfiable_pointers(child, pointer, parsed, out);
pointer.truncate(restore);
}
}
Value::Array(items) => {
let mut index_buffer = itoa::Buffer::new();
for (index, child) in items.iter().enumerate() {
pointer.push('/');
pointer.push_str(index_buffer.format(index));
collect_unsatisfiable_pointers(child, pointer, parsed, out);
pointer.truncate(restore);
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}