libikarus 0.1.14

The core functionality of Ikarus wrapped neatly in a rust library
Documentation
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");
        // nothing to do yet, settings aren't implemented
    }

    Ok(())
}