use crate::settings::{database, json_value::JsonValue, Scope, Validator};
use parking_lot::Mutex;
use std::sync::Arc;
use tracing::*;
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum Error {
#[error(transparent)]
ValidationError(#[from] crate::settings::validation::Error),
#[error("Schema for scope '{0}' does not exist.")]
SchemaNotFound(Scope),
#[error("Scope '{0}' does not exist.")]
ScopeNotFound(Scope),
#[error("Couldn't handle JSON. Error: {0}")]
JsonError(String),
#[error("Database error: {0}")]
DatabaseError(#[from] crate::settings::database::Error),
#[error(transparent)]
UpdateError(#[from] crate::settings::json_value::Error),
#[error("No valid settings found under scope '{0}'.")]
NoValidSettings(Scope),
#[error("No settings found at scope '{0}'.")]
NoSettingsAtScope(Scope),
#[error("Root scope is not allowed.")]
RootScopeNotAllowed,
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone)]
pub struct Repository {
database: Arc<Mutex<database::Database>>,
}
#[derive(Debug)]
pub struct SuccessfulValidation {
pub schema_scope: Scope,
pub object_with_defaults: serde_json::Value,
pub object_without_defaults: serde_json::Value,
}
fn validate(
schema_scope: &Scope,
validator: &Validator,
scope: &Scope,
settings: serde_json::Value,
global_settings: serde_json::Value,
) -> Result<SuccessfulValidation> {
debug!("Trying to validate {} with schema for {}", scope, schema_scope);
let updated_schema_settings_without_defaults = if schema_scope == scope {
settings
} else {
global_settings
.update_at(scope, settings)?
.pointer(schema_scope.as_json_ptr().as_str())
.cloned()
.unwrap() };
debug!("Validating {}", updated_schema_settings_without_defaults);
let res = validator
.validate_with_defaults(Some(&updated_schema_settings_without_defaults), scope)
.map(|object_with_defaults| SuccessfulValidation {
schema_scope: schema_scope.clone(),
object_with_defaults,
object_without_defaults: updated_schema_settings_without_defaults,
})?;
Ok(res)
}
fn stringify(value: &serde_json::Value) -> Result<String> {
serde_json::to_string(&value).map_err(|err| Error::JsonError(format!("{:?}", err)))
}
fn parse(s: String) -> Result<serde_json::Value> {
serde_json::from_str(s.as_str()).map_err(|err| Error::JsonError(format!("{:?}", err)))
}
fn parent_schema(tx: &mut database::Transaction, scope: &Scope) -> Result<(Scope, serde_json::Value)> {
let res = parent_schema0(tx, scope)?;
res.ok_or_else(|| Error::SchemaNotFound(scope.clone()))
}
fn parent_schema0(tx: &mut database::Transaction, scope: &Scope) -> Result<Option<(Scope, serde_json::Value)>> {
if scope.is_root() {
Ok(None)
} else {
match tx.get_schema(scope.to_string())? {
Some(schema) => {
let schema = parse(schema)?;
Ok(Some((scope.clone(), schema)))
}
None => parent_schema0(tx, &scope.drop_last()),
}
}
}
fn mk_validator(tx: &mut database::Transaction, scope: &Scope) -> Result<(Scope, Validator)> {
let (schema_scope, schema) = parent_schema(tx, scope)?;
let res = Validator::new(schema)?;
Ok((schema_scope, res))
}
impl Repository {
pub fn new(database: database::Database) -> Self {
Self {
database: Arc::new(Mutex::new(database)),
}
}
pub fn new_in_memory() -> Self {
Self::new(database::Database::in_memory().unwrap())
}
pub fn update_settings(
&self,
scope: &Scope,
settings: serde_json::Value,
force: bool,
) -> Result<serde_json::Value> {
self.database.lock().exec(|tx| {
let current_settings = tx
.get_settings()?
.map(parse)
.transpose()?
.unwrap_or_else(|| serde_json::json!({}));
let (schema_scope, validator) = mk_validator(tx, scope)?;
let validation = validate(
&schema_scope,
&validator,
scope,
settings.clone(),
current_settings.clone(),
);
match validation {
Ok(SuccessfulValidation {
schema_scope,
object_with_defaults: new_settings_with_defaults,
object_without_defaults: new_settings_without_defaults,
}) => {
debug!(
"Successful validation, new_settings_with_defaults: {}",
new_settings_with_defaults
);
let new_settings = current_settings.update_at(&schema_scope, new_settings_without_defaults)?;
tx.set_settings(stringify(&new_settings)?)?;
let new_settings_for_scope = if let Some(scope) = scope.diff(&schema_scope) {
new_settings_with_defaults
.pointer(scope.as_json_ptr().as_str())
.cloned()
.unwrap_or_default()
} else {
new_settings_with_defaults
};
Ok(new_settings_for_scope)
}
Err(Error::ValidationError(err)) if force => {
let new_settings = current_settings.update_at_force(scope, settings.clone());
info!(
"Validation failed with error {}. Force is enabled so {} will be set to {}.",
err, scope, new_settings
);
tx.set_settings(stringify(&new_settings)?)?;
Ok(settings)
}
Err(e) => Err(e), }
})?
}
pub fn clear_settings(&self, scope: &Scope) -> Result<()> {
self.database.lock().exec(|tx| {
if let Some(current_settings) = tx.get_settings()?.map(parse).transpose()? {
let new_settings = current_settings.remove_at(scope);
tx.set_settings(stringify(&new_settings)?)?;
}
Ok(())
})?
}
fn get_schema_settings(
tx: &mut database::Transaction,
current_settings: Option<&serde_json::Value>,
scope: &Scope,
no_defaults: bool,
) -> Result<Option<(Scope, serde_json::Value)>> {
let (schema_scope, validator) = mk_validator(tx, scope)?;
let schema_settings = current_settings.and_then(|c| c.pointer(&schema_scope.as_json_ptr()).cloned());
let res = if no_defaults {
schema_settings
} else {
Some(
validator
.validate_with_defaults(schema_settings.as_ref(), scope)
.map_err(|_| Error::NoValidSettings(scope.clone()))?,
)
};
Ok(res.map(|settings| (schema_scope, settings)))
}
pub fn get_settings(&self, scope: &Scope, no_defaults: bool) -> Result<serde_json::Value> {
self.database.lock().exec(|tx| {
let current_settings = tx.get_settings()?.map(parse).transpose()?;
if scope.is_root() {
if no_defaults {
current_settings.ok_or_else(|| Error::NoSettingsAtScope(scope.clone()))
} else {
let mut scopes = tx
.get_all_schema_scopes()?
.into_iter()
.map(|s| <Scope as std::convert::TryFrom<String>>::try_from(s).unwrap())
.collect::<Vec<Scope>>();
scopes.sort_by_key(|scope| scope.iter().len());
let all_settings_with_defaults = scopes
.into_iter()
.filter_map(|scope| {
Self::get_schema_settings(tx, current_settings.as_ref(), &scope, false).transpose()
})
.collect::<Result<Vec<(Scope, serde_json::Value)>>>()?
.into_iter()
.try_fold(serde_json::json!({}), |acc, (scope, settings)| {
acc.update_at(&scope, settings)
})?;
Ok(all_settings_with_defaults)
}
} else {
let scope_and_settings = Self::get_schema_settings(tx, current_settings.as_ref(), scope, no_defaults)?;
scope_and_settings
.and_then(|(schema_scope, settings)| match scope.diff(&schema_scope) {
Some(scope_within_schema) => settings.pointer(&scope_within_schema.as_json_ptr()).cloned(),
None => Some(settings),
})
.ok_or_else(|| Error::NoSettingsAtScope(scope.clone()))
}
})?
}
pub fn delete_schema(&self, scope: &Scope) -> Result<()> {
self.database.lock().exec(|tx| {
if !tx.delete_schema(scope.into())? {
return Err(Error::SchemaNotFound(scope.clone()));
}
if let Some(current_settings) = tx.get_settings()?.map(parse).transpose()? {
let new_settings = current_settings.remove_at(scope);
tx.set_settings(stringify(&new_settings)?)?;
}
Ok(())
})?
}
pub fn set_schema(&self, scope: &Scope, schema: serde_json::Value) -> Result<()> {
if scope.is_root() {
return Err(Error::RootScopeNotAllowed);
}
self.database.lock().exec(|tx| {
tx.set_schema(scope.into(), stringify(&schema)?)?;
Ok(())
})?
}
pub fn get_schema(&self, scope: &Scope) -> Result<serde_json::Value> {
self.database.lock().exec(|tx| {
let schema = tx
.get_schema(scope.into())?
.map(parse)
.transpose()?
.ok_or_else(|| Error::SchemaNotFound(scope.clone()))?;
Ok(schema)
})?
}
pub fn get_schema_scopes(&self) -> Result<Vec<String>> {
self.database.lock().exec(|tx| {
let schemas = tx.get_all_schema_scopes()?;
Ok(schemas)
})?
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn array() {
let repo = Repository::new_in_memory();
repo.set_schema(&"com.actyx".try_into().unwrap(), json!({})).unwrap();
repo.update_settings(&"com.actyx/a/b".try_into().unwrap(), json!([]), false)
.unwrap();
assert_eq!(
repo.get_settings(&"com.actyx".try_into().unwrap(), true).unwrap(),
json!({ "a": { "b": [] } })
);
repo.update_settings(&"com.actyx/a/b/0".try_into().unwrap(), json!("hello"), false)
.unwrap();
assert_eq!(
repo.get_settings(&"com.actyx".try_into().unwrap(), true).unwrap(),
json!({ "a": { "b": ["hello"] } })
);
repo.update_settings(&"com.actyx/a/b/0".try_into().unwrap(), json!("world"), false)
.unwrap();
assert_eq!(
repo.get_settings(&"com.actyx".try_into().unwrap(), true).unwrap(),
json!({ "a": { "b": ["world"] } })
);
}
}