libikarus 0.1.14

The core functionality of Ikarus wrapped neatly in a rust library
Documentation
use crate::args::validate::{
    validate_entity_value, validate_object_exists, validate_string_not_blank,
    EntityValueValidationData, EntityValueValidationError, ObjectValidationError,
    StringValidationError,
};
use crate::define_object;
use crate::objects::blueprint::Blueprint;
use crate::objects::entity_folder::EntityFolder;
use crate::objects::id::TypedId;
use crate::objects::object::*;
use crate::objects::property::Property;
use crate::objects::{ExpiredConnectionError, FromId, FromObjectScopeError};
use crate::persistence::project::ProjectError;
use crate::types::location::FolderPosition;
use serde_derive::{Deserialize, Serialize};
use sqrite::connection::{Connection, DbError, RcRefCellExtension};
use sqrite::sql_interface::{FromSql, FromSqlError, ToSql, ToSqlError};
use sqrite::value::Value;
use std::collections::HashSet;
use std::fmt::Debug;
use std::rc::Rc;
use tracing::*;

#[derive(thiserror::Error, Debug)]
pub enum EntityError {
    #[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 interface with entity as object: {0}")]
    ObjectError(#[from] ObjectHelperError),
    #[error("invalid scope: {0}")]
    InvalidScope(#[from] FromObjectScopeError),
}

#[derive(thiserror::Error, Debug)]
pub enum ValueError {
    #[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 (de-)serialize JSON to value: {0}")]
    SerdeError(#[from] serde_json::error::Error),
    #[error("property value failed validation: {0}")]
    EntityValueValidationError(#[from] EntityValueValidationError),
}

#[derive(Deserialize, Serialize, Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum ToggleValue {
    False,
    True,
    Third,
}

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

#[derive(Deserialize, Serialize, Debug, Clone, PartialOrd, PartialEq)]
pub enum EntityValue {
    Toggle(ToggleValue),
    Number(f64),
    Text(String),
}

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

impl FromSql for EntityValue {
    fn from_sql(value: Value) -> Result<Self, FromSqlError> {
        match value {
            Value::TextRef(v) => {
                Ok(serde_json::from_str(v).map_err(|e| FromSqlError::Custom(e.to_string()))?)
            }
            Value::Text(v) => {
                Ok(serde_json::from_str(&v).map_err(|e| FromSqlError::Custom(e.to_string()))?)
            }
            _ => Err(FromSqlError::UnexpectedType),
        }
    }
}

impl ToSql for EntityValue {
    fn to_sql(&self) -> Result<Value, ToSqlError> {
        Ok(Value::Text(
            serde_json::to_string(self).map_err(|e| ToSqlError::Custom(e.to_string()))?,
        ))
    }
}

impl ToSql for &EntityValue {
    fn to_sql(&self) -> Result<Value, ToSqlError> {
        Ok(Value::Text(
            serde_json::to_string(self).map_err(|e| ToSqlError::Custom(e.to_string()))?,
        ))
    }
}

pub struct EntityScope;

impl EntityScope {
    pub fn from_object_scope(scope: ObjectScope) -> Result<EntityScope, FromObjectScopeError> {
        match scope {
            ObjectScope::Entities => Ok(EntityScope),
            v => Err(FromObjectScopeError::InvalidScope("entity", v)),
        }
    }

    pub fn to_object_scope(&self) -> ObjectScope {
        ObjectScope::Entities
    }
}

define_object!(Entity, EntityFolder, EntityScope, EntityError);

impl Entity {
    #[instrument(level = Level::INFO, skip(conn, name, information), fields(name = name.as_ref()))]
    pub fn new(
        conn: Rc<Connection>,
        blueprints: HashSet<&Blueprint>,
        parent_folder: Option<&EntityFolder>,
        position: FolderPosition,
        name: impl AsRef<str>,
        information: impl AsRef<str>,
    ) -> Result<Self, EntityError> {
        validate_string_not_blank(&name)?;

        let entity = conn.transaction_rc(|conn| {
            let id = object_create(
                conn.clone(),
                ObjectType::Entity,
                ObjectScope::Entities,
                parent_folder.map(|v| v.to_folder()),
                position,
                name,
                information,
            )
            .map_err(|e| DbError::TransactionError(e.to_string()))?;

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

            for blueprint in blueprints {
                conn.execute(
                    "INSERT INTO `entity_blueprints`(`entity`, `blueprint`) VALUES(?, ?)",
                    (id, blueprint.id()),
                )?;
            }

            Ok(Entity {
                conn: Rc::downgrade(&conn),
                id,
            })
        })?;

        Ok(entity)
    }

    #[instrument(level = Level::DEBUG)]
    pub fn get_blueprints(&self) -> Result<Vec<Blueprint>, EntityError> {
        validate_object_exists(self.to_object())?;

        let conn = self.conn()?;

        Ok(conn.query_all(
            "SELECT `blueprint` FROM `entity_blueprints` WHERE `entity` = ?",
            self.id(),
            |row| {
                Ok(Blueprint::from_id(conn.clone(), row.get::<TypedId>(0)?)
                    .map_err(|e| FromSqlError::Custom(e.to_string()))?)
            },
        )?)
    }

    #[instrument(level = Level::DEBUG)]
    pub fn is_default_value(&self, property: &Property) -> Result<bool, ValueError> {
        validate_object_exists(self.to_object())?;
        validate_object_exists(property.to_object())?;

        let ret = self.conn()?.query_one(
            "SELECT NOT EXISTS(SELECT 1 FROM `values` WHERE `entity` = ? AND `property` = ?)",
            (self.id(), property.id()),
            |row| row.get::<bool>(0),
        )?;

        Ok(ret)
    }

    #[instrument(level = Level::DEBUG)]
    pub fn value(&self, property: &Property) -> Result<EntityValue, ValueError> {
        validate_object_exists(self.to_object())?;
        validate_object_exists(property.to_object())?;

        validate_entity_value(
            EntityValueValidationData::EntityAndProperty(self, property),
            None,
        )?;

        let value_json = self.conn()?.query_one(
            "SELECT IFNULL((SELECT `value` FROM `values` WHERE `entity` = ? AND `property` = ?), (SELECT `default_value` FROM `properties` WHERE `id` = ?))",
            (self.id(), property.id(), property.id()),
            |row| row.get::<String>(0),
        )?;

        Ok(serde_json::from_str(&value_json)?)
    }

    #[instrument(level = Level::INFO)]
    pub fn set_value(
        &mut self,
        property: &Property,
        new_value: &EntityValue,
    ) -> Result<(), ValueError> {
        validate_object_exists(self.to_object())?;
        validate_object_exists(property.to_object())?;

        validate_entity_value(
            EntityValueValidationData::EntityAndProperty(self, property),
            Some(new_value),
        )?;

        self.conn()?.execute(
            r#"
                INSERT INTO `values`(`entity`, `property`, `value`) VALUES(?, ?, ?)
                ON CONFLICT(`entity`, `property`) DO UPDATE SET `value` = EXCLUDED.`value`
                WHERE `entity` = ? AND `property` = ?
            "#,
            (
                self.id(),
                property.id(),
                new_value,
                self.id(),
                property.id(),
            ),
        )?;

        Ok(())
    }

    #[instrument(level = Level::INFO)]
    pub fn reset_value(&self, property: &Property) -> Result<(), ValueError> {
        validate_object_exists(self.to_object())?;
        validate_object_exists(property.to_object())?;

        self.conn()?.execute(
            r#"
                DELETE FROM `values` WHERE `entity` = ? AND `property` = ?
            "#,
            (self.id(), property.id()),
        )?;

        Ok(())
    }
}