use serde_json::Value;
use crate::gts::GTS_ID_URI_PREFIX;
use crate::store::StoreError;
pub(crate) trait SchemaProvider {
fn schema_content(&self, type_id: &str) -> Option<&Value>;
}
const MAX_REF_CHAIN_DEPTH: usize = 32;
pub(crate) struct SchemaResolver<'a> {
provider: &'a dyn SchemaProvider,
}
impl<'a> SchemaResolver<'a> {
pub(crate) fn new(provider: &'a dyn SchemaProvider) -> Self {
Self { provider }
}
pub(crate) fn resolve(&self, schema: &Value) -> Result<Value, StoreError> {
let mut visited = std::collections::HashSet::new();
let mut cycle_found = false;
let mut unresolved_refs = Vec::new();
let resolved = self.resolve_inner(
schema,
schema,
&mut visited,
&mut cycle_found,
&mut unresolved_refs,
);
if cycle_found {
Err(StoreError::CircularRef)
} else if !unresolved_refs.is_empty() {
Err(StoreError::UnresolvedRefs(unresolved_refs))
} else {
Ok(resolved)
}
}
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
fn resolve_inner(
&self,
schema: &Value,
local_root: &Value,
visited: &mut std::collections::HashSet<String>,
cycle_found: &mut bool,
unresolved_refs: &mut Vec<String>,
) -> Value {
match schema {
Value::Object(map) => {
if let Some(Value::String(ref_uri)) = map.get("$ref") {
match ref_uri.as_str() {
"#/$defs/GtsInstanceId" => {
return crate::GtsInstanceId::json_schema_value();
}
"#/$defs/GtsTypeId" | "#/$defs/GtsSchemaId" => {
return crate::GtsTypeId::json_schema_value();
}
s if s == "#" || s.starts_with("#/") => {
if let Some(pointer) = ref_uri.strip_prefix('#') {
let local_ref_key =
format!("local:{:p}:{ref_uri}", std::ptr::from_ref(local_root));
if visited.contains(&local_ref_key) {
*cycle_found = true;
return Value::Object(map.clone());
}
if visited.len() >= MAX_REF_CHAIN_DEPTH {
unresolved_refs.push(format!(
"{ref_uri} (nested deeper than \
{MAX_REF_CHAIN_DEPTH} chained $refs)"
));
return Value::Object(map.clone());
}
if let Some(target) = local_root.pointer(pointer) {
visited.insert(local_ref_key.clone());
let resolved = self.resolve_inner(
target,
local_root,
visited,
cycle_found,
unresolved_refs,
);
visited.remove(&local_ref_key);
return self.resolved_ref_with_siblings(
map,
resolved,
local_root,
visited,
cycle_found,
unresolved_refs,
);
}
unresolved_refs.push(ref_uri.clone());
}
let mut new_map = serde_json::Map::new();
for (k, v) in map {
new_map.insert(
k.clone(),
self.resolve_inner(
v,
local_root,
visited,
cycle_found,
unresolved_refs,
),
);
}
return Value::Object(new_map);
}
_ => {} }
let canonical_ref = ref_uri.strip_prefix(GTS_ID_URI_PREFIX).unwrap_or(ref_uri);
let (lookup_ref, pointer_fragment) =
if let Some((id, fragment)) = canonical_ref.split_once('#') {
let pointer = if fragment.is_empty() {
Some("")
} else if fragment.starts_with('/') {
Some(fragment)
} else {
None
};
(id, pointer)
} else {
(canonical_ref, None)
};
if visited.contains(canonical_ref) {
*cycle_found = true;
let mut new_map = serde_json::Map::new();
for (k, v) in map {
new_map.insert(
k.clone(),
if k == "$ref" {
v.clone()
} else {
self.resolve_inner(
v,
local_root,
visited,
cycle_found,
unresolved_refs,
)
},
);
}
return Value::Object(new_map);
}
if visited.len() >= MAX_REF_CHAIN_DEPTH {
unresolved_refs.push(format!(
"{canonical_ref} (nested deeper than \
{MAX_REF_CHAIN_DEPTH} chained $refs)"
));
return Value::Object(map.clone());
}
if let Some(content) = self.provider.schema_content(lookup_ref) {
let target_content = match pointer_fragment {
Some("") => Some(content),
Some(pointer) => content.pointer(pointer),
None if canonical_ref.contains('#') => None,
None => Some(content),
};
if let Some(target_content) = target_content {
visited.insert(canonical_ref.to_owned());
let mut resolved = self.resolve_inner(
target_content,
content,
visited,
cycle_found,
unresolved_refs,
);
visited.remove(canonical_ref);
if let Value::Object(ref mut resolved_map) = resolved {
resolved_map.remove("$id");
resolved_map.remove("$schema");
resolved_map.remove(crate::schema_modifiers::X_GTS_FINAL);
resolved_map.remove(crate::schema_modifiers::X_GTS_ABSTRACT);
resolved_map.remove(crate::schema_traits::X_GTS_TRAITS);
resolved_map.remove(crate::schema_traits::X_GTS_TRAITS_SCHEMA);
}
return self.resolved_ref_with_siblings(
map,
resolved,
local_root,
visited,
cycle_found,
unresolved_refs,
);
}
}
if !ref_uri.starts_with('#') {
unresolved_refs.push(ref_uri.clone());
}
let mut new_map = serde_json::Map::new();
for (k, v) in map {
new_map.insert(
k.clone(),
if k == "$ref" {
v.clone()
} else {
self.resolve_inner(
v,
local_root,
visited,
cycle_found,
unresolved_refs,
)
},
);
}
return Value::Object(new_map);
}
let mut new_map = serde_json::Map::new();
for (k, v) in map {
new_map.insert(
k.clone(),
self.resolve_inner(v, local_root, visited, cycle_found, unresolved_refs),
);
}
Value::Object(new_map)
}
Value::Array(arr) => Value::Array(
arr.iter()
.map(|v| {
self.resolve_inner(v, local_root, visited, cycle_found, unresolved_refs)
})
.collect(),
),
_ => schema.clone(),
}
}
fn resolved_ref_with_siblings(
&self,
map: &serde_json::Map<String, Value>,
resolved: Value,
local_root: &Value,
visited: &mut std::collections::HashSet<String>,
cycle_found: &mut bool,
unresolved_refs: &mut Vec<String>,
) -> Value {
if map.len() == 1 {
return resolved;
}
match resolved {
Value::Object(resolved_map) => {
let mut siblings = serde_json::Map::new();
for (k, v) in map {
if k != "$ref" {
siblings.insert(
k.clone(),
self.resolve_inner(
v,
local_root,
visited,
cycle_found,
unresolved_refs,
),
);
}
}
if siblings.is_empty() {
return Value::Object(resolved_map);
}
let mut merged = serde_json::Map::new();
merged.insert(
"allOf".to_owned(),
Value::Array(vec![Value::Object(resolved_map), Value::Object(siblings)]),
);
Value::Object(merged)
}
other => other,
}
}
}
#[cfg(test)]
#[path = "schema_resolver_test.rs"]
mod schema_resolver_test;