use semver::Version;
use super::{SchemaEntry, SchemaOrigin, SchemaRegistrationError};
pub struct VersionedSchemaRegistry {
entries: Vec<SchemaEntry>,
#[cfg(not(target_arch = "wasm32"))]
compiled: std::sync::RwLock<
std::collections::HashMap<(String, Version), std::sync::Arc<jsonschema::JSONSchema>>,
>,
}
impl VersionedSchemaRegistry {
#[must_use]
pub fn new() -> Self {
let entries = super::embedded::initial_entries();
Self {
entries,
#[cfg(not(target_arch = "wasm32"))]
compiled: std::sync::RwLock::new(std::collections::HashMap::new()),
}
}
fn check_compilable(json: &str) -> Result<(), SchemaRegistrationError> {
let value: serde_json::Value = serde_json::from_str(json)
.map_err(|e| SchemaRegistrationError::InvalidJson(e.to_string()))?;
#[cfg(not(target_arch = "wasm32"))]
jsonschema::JSONSchema::compile(&value).map_err(|e| {
SchemaRegistrationError::InvalidJson(format!("schema does not compile: {e}"))
})?;
#[cfg(target_arch = "wasm32")]
let _ = value;
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn evict(&mut self, sector: &str, version: &Version) {
self.compiled
.get_mut()
.expect("schema cache not poisoned")
.remove(&(sector.to_owned(), version.clone()));
}
#[cfg(target_arch = "wasm32")]
fn evict(&mut self, _sector: &str, _version: &Version) {}
pub fn register(
&mut self,
sector: &str,
version_str: &str,
json: String,
) -> Result<(), SchemaRegistrationError> {
let version: Version = version_str
.parse()
.map_err(|_| SchemaRegistrationError::InvalidVersion(version_str.to_owned()))?;
Self::check_compilable(&json)?;
if self
.entries
.iter()
.any(|e| e.sector == sector && e.version == version)
{
return Err(SchemaRegistrationError::AlreadyExists {
sector: sector.to_owned(),
version,
});
}
self.entries.push(SchemaEntry {
sector: sector.to_owned(),
version,
json,
origin: SchemaOrigin::Runtime,
});
Ok(())
}
pub fn register_or_replace(
&mut self,
sector: &str,
version_str: &str,
json: String,
) -> Result<bool, SchemaRegistrationError> {
let version: Version = version_str
.parse()
.map_err(|_| SchemaRegistrationError::InvalidVersion(version_str.to_owned()))?;
Self::check_compilable(&json)?;
let replaced = if let Some(existing) = self
.entries
.iter_mut()
.find(|e| e.sector == sector && e.version == version)
{
existing.json = json;
existing.origin = SchemaOrigin::Runtime;
true
} else {
self.entries.push(SchemaEntry {
sector: sector.to_owned(),
version: version.clone(),
json,
origin: SchemaOrigin::Runtime,
});
false
};
if replaced {
self.evict(sector, &version);
}
Ok(replaced)
}
pub fn unregister(&mut self, sector: &str, version: &Version) -> bool {
let before = self.entries.len();
self.entries.retain(|e| {
!(e.sector == sector && e.version == *version && e.origin == SchemaOrigin::Runtime)
});
let removed = self.entries.len() < before;
if removed {
self.evict(sector, version);
}
removed
}
pub fn get(&self, sector: &str, version: &Version) -> Option<&str> {
self.entries
.iter()
.find(|e| e.sector == sector && e.version == *version)
.map(|e| e.json.as_str())
}
pub fn get_entry(&self, sector: &str, version: &Version) -> Option<&SchemaEntry> {
self.entries
.iter()
.find(|e| e.sector == sector && e.version == *version)
}
pub fn latest(&self, sector: &str) -> Option<(&Version, &str)> {
self.entries
.iter()
.filter(|e| e.sector == sector)
.max_by(|a, b| a.version.cmp(&b.version))
.map(|e| (&e.version, e.json.as_str()))
}
pub fn list(&self) -> Vec<(&str, &Version)> {
self.entries
.iter()
.map(|e| (e.sector.as_str(), &e.version))
.collect()
}
pub fn versions_for(&self, sector: &str) -> Vec<&Version> {
let mut versions: Vec<&Version> = self
.entries
.iter()
.filter(|e| e.sector == sector)
.map(|e| &e.version)
.collect();
versions.sort();
versions
}
pub fn sectors(&self) -> Vec<&str> {
let mut sectors: Vec<&str> = self.entries.iter().map(|e| e.sector.as_str()).collect();
sectors.sort();
sectors.dedup();
sectors
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn validate_strict(
&self,
sector: &str,
version: &str,
data: &serde_json::Value,
) -> Result<(), crate::domain::field_error::ValidationErrors> {
use crate::domain::field_error::{FieldError, ValidationErrors};
let version = version.parse::<Version>().map_err(|_| ValidationErrors {
errors: vec![FieldError {
field: "/schema_version".to_owned(),
message: format!("schema version '{version}' is not a valid semver string"),
}],
})?;
self.validate(sector, &version, data)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn validate_if_present(
&self,
sector: &str,
version: &str,
data: &serde_json::Value,
) -> Result<(), crate::domain::field_error::ValidationErrors> {
let Ok(version) = version.parse::<Version>() else {
return Ok(());
};
if self.get(sector, &version).is_none() {
return Ok(());
}
self.validate(sector, &version, data)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn validate(
&self,
sector: &str,
version: &Version,
data: &serde_json::Value,
) -> Result<(), crate::domain::field_error::ValidationErrors> {
use crate::domain::field_error::{FieldError, ValidationErrors};
let compiled = self
.compiled_schema(sector, version)
.ok_or_else(|| ValidationErrors {
errors: vec![FieldError {
field: "/".to_owned(),
message: format!("no schema found for sector '{sector}' version '{version}'"),
}],
})?;
match compiled.validate(data) {
Ok(()) => Ok(()),
Err(errors) => Err(ValidationErrors {
errors: errors
.map(|e| FieldError {
field: e.instance_path.to_string(),
message: e.to_string(),
})
.collect(),
}),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn compiled_schema(
&self,
sector: &str,
version: &Version,
) -> Option<std::sync::Arc<jsonschema::JSONSchema>> {
let key = (sector.to_owned(), version.clone());
if let Some(cached) = self
.compiled
.read()
.expect("schema cache not poisoned")
.get(&key)
{
return Some(cached.clone());
}
let json = self.get(sector, version)?;
let value = serde_json::from_str::<serde_json::Value>(json).ok()?;
let compiled = std::sync::Arc::new(jsonschema::JSONSchema::compile(&value).ok()?);
self.compiled
.write()
.expect("schema cache not poisoned")
.insert(key, compiled.clone());
Some(compiled)
}
}
impl Default for VersionedSchemaRegistry {
fn default() -> Self {
Self::new()
}
}