use crate::addons::bool::BoolToResult;
use crate::objects::blueprint::*;
use crate::objects::entity::*;
use crate::objects::folder::*;
use crate::objects::object::*;
use crate::objects::property::*;
use crate::objects::property_settings::*;
use crate::objects::ExpiredConnectionError;
use crate::persistence::project::ProjectError;
use crate::types::location::FolderPosition;
use sqrite::connection::{Connection, DbError};
use std::io;
use std::path::Path;
use std::rc::Rc;
use tracing::*;
#[instrument(level = Level::DEBUG, skip(validation_func))]
pub fn validate_optional_arg<T: std::fmt::Debug, F, E>(
arg: Option<T>,
validation_func: F,
) -> Result<(), E>
where
F: FnOnce(T) -> Result<(), E>,
{
if let Some(arg) = arg {
validation_func(arg)?;
}
Ok(())
}
#[instrument(level = Level::DEBUG, skip(validation_func))]
pub fn validate_list_args<T: std::fmt::Debug, F, E>(
args: Vec<T>,
validation_func: F,
) -> Result<(), E>
where
F: Fn(T) -> Result<(), E>,
{
for arg in args {
validation_func(arg)?;
}
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum StringValidationError {
#[error("string is blank")]
InvalidBlank,
}
#[instrument(level = Level::DEBUG, skip(str))]
pub fn validate_string_not_blank(str: impl AsRef<str>) -> Result<(), StringValidationError> {
str.as_ref()
.trim()
.is_empty()
.false_or(StringValidationError::InvalidBlank)?;
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum PathValidationError {
#[error("unable to interface with IO: {0}")]
IoInterfaceError(#[from] io::Error),
#[error("path parent doesn't exist")]
MissingParent,
}
#[instrument(level = Level::DEBUG, skip(path), fields(path = path.as_ref().display().to_string()))]
pub fn validate_path_parent_exists(path: impl AsRef<Path>) -> Result<(), PathValidationError> {
let path = path.as_ref();
if path.is_relative() {
std::env::current_dir()?
.join(path)
.parent()
.is_some_and(|p| p.exists())
} else {
path.parent().is_some_and(|p| p.exists())
}
.true_or(PathValidationError::MissingParent)?;
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum ObjectValidationError {
#[error("failed to interface with project: {0}")]
ProjectError(#[from] ProjectError),
#[error("connection has expired")]
ExpiredConnection(#[from] ExpiredConnectionError),
#[error("failed to interface with database: {0}")]
DatabaseError(#[from] DbError),
#[error("object had invalid type: {0}")]
InvalidType(#[from] ObjectTypeValidationError),
#[error("object doesn't exist")]
MissingObject,
}
#[instrument(level = Level::DEBUG)]
pub fn validate_object_type(
object: Object,
expected: ObjectTypes,
) -> Result<(), ObjectValidationError> {
expected.validate(object)?;
Ok(())
}
#[instrument(level = Level::DEBUG)]
pub fn validate_object_exists(object: Object) -> Result<(), ObjectValidationError> {
object
.conn()?
.query_one(
"SELECT EXISTS(SELECT 1 FROM `objects` WHERE `id` = ?)",
object.id(),
|row| row.get::<bool>(0),
)?
.true_or(ObjectValidationError::MissingObject)?;
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum FolderPositionValidationError {
#[error("position {0} is out of bounds. Parent only has {1} children.")]
OutOfBounds(FolderPosition, usize),
#[error("error interfacing with folder API: {0}")]
FolderError(Box<FolderHelperError>),
}
#[instrument(level = Level::DEBUG, skip(conn, folder_position))]
pub fn validate_position_in_bounds(
conn: Rc<Connection>,
folder_position: FolderPosition,
folder_or_scope: FolderOrScope,
insert: bool,
) -> Result<(), FolderPositionValidationError> {
match folder_position {
FolderPosition::StartOfFolder => Ok(()),
FolderPosition::At(pos) => {
let children_count = folder_get_children_count(conn.clone(), folder_or_scope)
.map_err(|e| FolderPositionValidationError::FolderError(Box::new(e)))?;
(if insert {
pos <= children_count
} else {
pos < children_count
})
.true_or(FolderPositionValidationError::OutOfBounds(
folder_position,
children_count,
))?;
Ok(())
}
FolderPosition::EndOfFolder => Ok(()),
}
}
#[derive(thiserror::Error, Debug)]
pub enum FolderOrScopeValidationError {
#[error("failed to interface with database: {0}")]
DbError(#[from] DbError),
#[error("connection has expired")]
ExpiredConnectionError(#[from] ExpiredConnectionError),
#[error("folder or scope doesn't exist")]
MissingFolderOrScope(#[from] ObjectValidationError),
}
#[instrument(level = Level::DEBUG, skip(folder_or_scope))]
pub fn validate_folder_or_scope_exists(
folder_or_scope: &FolderOrScope,
) -> Result<(), FolderOrScopeValidationError> {
match folder_or_scope {
FolderOrScope::Folder(folder) => {
validate_object_exists(folder.to_object())?;
}
FolderOrScope::Scope(scope) => match scope {
ObjectScope::Blueprints => {}
ObjectScope::Property(PropertyScope::Blueprint(blueprint)) => {
validate_object_exists(blueprint.to_object())?;
}
ObjectScope::Property(PropertyScope::Entity(entity)) => {
validate_object_exists(entity.to_object())?;
}
ObjectScope::Entities => {}
},
}
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum PropertySettingsValidationError {
#[error("failed to interface with database: {0}")]
DbError(#[from] DbError),
#[error("settings have invalid type, property is of type {0}")]
InvalidSettingsType(PropertyType),
#[error("unable to interface with property: {0}")]
PropertyError(#[from] Box<PropertyError>),
}
#[derive(Debug, Clone)]
pub enum PropertySettingsValidationData {
Property(Property),
PropertyType(PropertyType),
}
#[instrument(level = Level::DEBUG, skip(property_settings))]
pub fn validate_property_settings(
data: PropertySettingsValidationData,
property_settings: &PropertySettings,
) -> Result<(), PropertySettingsValidationError> {
let property_type = match data {
PropertySettingsValidationData::Property(property) => {
property.r#type().map_err(Box::new)?
}
PropertySettingsValidationData::PropertyType(property_type) => property_type,
};
match property_settings {
PropertySettings::Toggle { .. } => property_type == PropertyType::Toggle,
PropertySettings::Number { .. } => property_type == PropertyType::Number,
PropertySettings::Text { .. } => property_type == PropertyType::Text,
}
.true_or(PropertySettingsValidationError::InvalidSettingsType(
property_type,
))?;
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum EntityValueValidationError {
#[error("failed to interface with database: {0}")]
DbError(#[from] DbError),
#[error(
"value wasn't applied to the correct scope. The entity is scoped to blueprints {0:?} but the attribute is scoped to {1}"
)]
MismatchedBlueprintScope(Vec<Blueprint>, PropertyScope),
#[error("value wasn't applied to the correct scope. The property is scoped to {0}")]
MismatchedEntityScope(PropertyScope),
#[error("value has an invalid type, property is of type {0}")]
InvalidValueType(PropertyType),
#[error("unable to interface with property: {0}")]
PropertyError(#[from] Box<PropertyError>),
#[error("unable to interface with entity: {0}")]
EntityError(#[from] Box<EntityError>),
}
#[derive(Debug, Clone)]
pub enum EntityValueValidationData<'a> {
Property(&'a Property),
PropertyType(PropertyType),
EntityAndProperty(&'a Entity, &'a Property),
}
#[instrument(level = Level::DEBUG)]
pub fn validate_entity_value(
data: EntityValueValidationData,
entity_value: Option<&EntityValue>,
) -> Result<(), EntityValueValidationError> {
let property_type = match &data {
EntityValueValidationData::Property(property) => property.r#type().map_err(Box::new)?,
EntityValueValidationData::PropertyType(property_type) => property_type.clone(),
EntityValueValidationData::EntityAndProperty(_, property) => {
property.r#type().map_err(Box::new)?
}
};
if let EntityValueValidationData::EntityAndProperty(entity, property) = &data {
debug!("validating property scope matches entity scope");
let property_scope = property.scope().map_err(Box::new)?;
match &property_scope {
PropertyScope::Blueprint(blueprint_scope) => {
let blueprints = entity.get_blueprints().map_err(Box::new)?;
if !blueprints.contains(&blueprint_scope) {
return Err(EntityValueValidationError::MismatchedBlueprintScope(
blueprints,
property_scope,
));
}
}
PropertyScope::Entity(entity_scope) if entity != entity_scope => {
return Err(EntityValueValidationError::MismatchedEntityScope(
property_scope,
));
}
_ => {}
}
}
if let Some(entity_value) = entity_value {
debug!("validating property value type matches property type");
match entity_value {
EntityValue::Toggle(_) => property_type == PropertyType::Toggle,
EntityValue::Number(_) => property_type == PropertyType::Number,
EntityValue::Text(_) => property_type == PropertyType::Text,
}
.true_or(EntityValueValidationError::InvalidValueType(property_type))?;
debug!("validating property value matches settings");
}
Ok(())
}