libikarus 0.1.14

The core functionality of Ikarus wrapped neatly in a rust library
Documentation
use crate::args::validate::{
    validate_folder_or_scope_exists, validate_position_in_bounds, FolderOrScopeValidationError,
    FolderPositionValidationError, ObjectValidationError, StringValidationError,
};
use crate::objects::blueprint_folder::BlueprintFolder;
use crate::objects::entity_folder::EntityFolder;
use crate::objects::id::TypedId;
use crate::objects::object::{object_create, Object, ObjectScope, ObjectType};
use crate::objects::property_folder::PropertyFolder;
use crate::objects::{ExpiredConnectionError, FromId, FromIdError};
use crate::persistence::project::ProjectError;
use crate::types::location::FolderPosition;
use sqrite::connection::{Connection, DbError, RcRefCellExtension};
use sqrite::sql_interface::FromSqlError;
use std::fmt::Formatter;
use std::rc::Rc;
use tracing::*;

#[derive(thiserror::Error, Debug)]
pub enum FolderHelperError {
    #[error("error interfacing with project: {0}")]
    ProjectError(#[from] ProjectError),
    #[error("connection has expired")]
    ExpiredConnection(#[from] ExpiredConnectionError),
    #[error("error interfacing with database: {0}")]
    DbError(#[from] DbError),
    #[error("unable to validate object: {0}")]
    ObjectValidationError(#[from] ObjectValidationError),
    #[error("unable to validate string argument: {0}")]
    StringValidationError(#[from] StringValidationError),
    #[error("unable to validate folder position argument: {0}")]
    FolderPositionValidationError(#[from] FolderPositionValidationError),
    #[error("unable to validate folder or scope argument: {0}")]
    FolderOrScopeValidationError(#[from] FolderOrScopeValidationError),
    #[error("unable to construct folder from id {0} with object type {1}")]
    InvalidId(TypedId, ObjectType),
}

#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum FolderOrScope {
    Folder(Folder),
    Scope(ObjectScope),
}

impl FolderOrScope {
    pub fn from_opt_folder(folder: Option<Folder>, scope: ObjectScope) -> FolderOrScope {
        if let Some(folder) = folder {
            FolderOrScope::Folder(folder)
        } else {
            FolderOrScope::Scope(scope)
        }
    }
}

impl std::fmt::Display for FolderOrScope {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Folder {
    BlueprintFolder(BlueprintFolder),
    PropertyFolder(PropertyFolder),
    EntityFolder(EntityFolder),
}

impl std::fmt::Display for Folder {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl FromId for Folder {
    fn from_id(conn: Rc<Connection>, id: TypedId) -> Result<Self, FromIdError>
    where
        Self: Sized,
    {
        match id.get_object_type() {
            ObjectType::BlueprintFolder => Ok(Folder::BlueprintFolder(BlueprintFolder {
                id,
                conn: Rc::downgrade(&conn),
            })),
            ObjectType::PropertyFolder => Ok(Folder::PropertyFolder(PropertyFolder {
                id,
                conn: Rc::downgrade(&conn),
            })),
            ObjectType::EntityFolder => Ok(Folder::EntityFolder(EntityFolder {
                id,
                conn: Rc::downgrade(&conn),
            })),
            _ => Err(FromIdError::InvalidId("folder", id)),
        }
    }
}

impl Folder {
    #[instrument(level = Level::TRACE, skip(conn))]
    pub(crate) fn from_id(id: TypedId, conn: Rc<Connection>) -> Result<Folder, FolderHelperError> {
        Ok(match id.get_object_type() {
            ObjectType::BlueprintFolder => Folder::BlueprintFolder(BlueprintFolder {
                conn: Rc::downgrade(&conn),
                id,
            }),
            ObjectType::PropertyFolder => Folder::PropertyFolder(PropertyFolder {
                conn: Rc::downgrade(&conn),
                id,
            }),
            ObjectType::EntityFolder => Folder::EntityFolder(EntityFolder {
                conn: Rc::downgrade(&conn),
                id,
            }),
            v => return Err(FolderHelperError::InvalidId(id, v)),
        })
    }

    pub fn conn(&self) -> Result<Rc<Connection>, ExpiredConnectionError> {
        match self {
            Folder::BlueprintFolder(BlueprintFolder { conn, .. }) => conn,
            Folder::PropertyFolder(PropertyFolder { conn, .. }) => conn,
            Folder::EntityFolder(EntityFolder { conn, .. }) => conn,
        }
        .upgrade()
        .ok_or(ExpiredConnectionError)
    }

    pub(crate) fn id(&self) -> TypedId {
        *match self {
            Folder::BlueprintFolder(BlueprintFolder { id, .. }) => id,
            Folder::PropertyFolder(PropertyFolder { id, .. }) => id,
            Folder::EntityFolder(EntityFolder { id, .. }) => id,
        }
    }

    pub fn to_object(&self) -> Object {
        match self {
            Folder::BlueprintFolder(folder) => Object::BlueprintFolder(folder.clone()),
            Folder::PropertyFolder(folder) => Object::PropertyFolder(folder.clone()),
            Folder::EntityFolder(folder) => Object::EntityFolder(folder.clone()),
        }
    }
}

pub(crate) fn folder_create(
    conn: Rc<Connection>,
    object_scope: ObjectScope,
    object_type: ObjectType,
    parent_folder: Option<Folder>,
    position: FolderPosition,
    name: impl AsRef<str>,
    information: impl AsRef<str>,
) -> Result<TypedId, FolderHelperError> {
    validate_position_in_bounds(
        conn.clone(),
        position,
        FolderOrScope::from_opt_folder(parent_folder.clone(), object_scope.clone()),
        true,
    )?;

    let folder = conn.transaction_rc(|conn| {
        let folder = object_create(
            conn.clone(),
            object_type,
            object_scope,
            parent_folder,
            position,
            name,
            information,
        )
        .map_err(|e| DbError::TransactionError(e.to_string()))?;

        conn.execute("INSERT INTO `folders`(`id`) VALUES(?)", folder)?;

        Ok(folder)
    })?;

    Ok(folder)
}

#[instrument(level = Level::TRACE, skip(conn))]
pub(crate) fn folder_get_children<T: FromId>(
    conn: Rc<Connection>,
    folder_or_scope: FolderOrScope,
) -> Result<Vec<T>, FolderHelperError> {
    validate_folder_or_scope_exists(&folder_or_scope)?;

    Ok(match folder_or_scope {
        FolderOrScope::Folder(folder) => conn.query_all(
            r#"
                SELECT `object`
                FROM `object_tree`
                WHERE `parent` = ?
            "#,
            folder.id(),
            |row| {
                Ok(T::from_id(conn.clone(), row.get::<TypedId>(0)?)
                    .map_err(|e| FromSqlError::Custom(e.to_string()))?)
            },
        )?,
        FolderOrScope::Scope(scope) => conn.query_all(
            r#"
                SELECT `tree`.`object`
                FROM `object_tree` AS `tree` 
                INNER JOIN `object_scopes` USING (`object`)
                WHERE `scope` = ? AND `parent` IS NULL
            "#,
            scope,
            |row| {
                Ok(T::from_id(conn.clone(), row.get::<TypedId>(0)?)
                    .map_err(|e| FromSqlError::Custom(e.to_string()))?)
            },
        )?,
    })
}

pub(crate) fn folder_get_children_count(
    conn: Rc<Connection>,
    folder_or_scope: FolderOrScope,
) -> Result<usize, FolderHelperError> {
    validate_folder_or_scope_exists(&folder_or_scope)?;

    Ok(match folder_or_scope {
        FolderOrScope::Folder(folder) => conn.query_one(
            r#"
                SELECT COUNT(`object`)
                FROM `object_tree`
                WHERE `parent` = ?
            "#,
            folder.id(),
            |row| row.get::<usize>(0),
        )?,
        FolderOrScope::Scope(scope) => conn.query_one(
            r#"
                SELECT COUNT(`tree`.`object`)
                FROM `object_tree` AS `tree` 
                INNER JOIN `object_scopes` USING (`object`)
                WHERE `scope` = ? AND `parent` IS NULL
            "#,
            scope,
            |row| row.get::<usize>(0),
        )?,
    })
}