libikarus 0.1.14

The core functionality of Ikarus wrapped neatly in a rust library
Documentation
use crate::args::validate::*;
use crate::define_object;
use crate::objects::blueprint::Blueprint;
use crate::objects::entity::{Entity, EntityValue, ToggleValue};
use crate::objects::id::TypedId;
use crate::objects::object::*;
use crate::objects::property_folder::PropertyFolder;
use crate::objects::property_settings::number_settings::NumberSettings;
use crate::objects::property_settings::text_settings::TextSettings;
use crate::objects::property_settings::toggle_settings::ToggleSettings;
use crate::objects::property_settings::PropertySettings;
use crate::objects::{ExpiredConnectionError, FromObjectScopeError};
use crate::persistence::project::ProjectError;
use crate::types::location::FolderPosition;
use sqrite::connection::{Connection, DbError, RcRefCellExtension};
use sqrite::sql_interface::{FromSql, FromSqlError, ToSql, ToSqlError};
use sqrite::value::Value;
use std::fmt::Formatter;
use std::rc::Rc;
use tracing::*;

#[derive(thiserror::Error, Debug)]
pub enum PropertyError {
    #[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("property settings failed validation: {0}")]
    PropertySettingsValidationError(#[from] PropertySettingsValidationError),
    #[error("property value failed validation: {0}")]
    EntityValueValidationError(#[from] EntityValueValidationError),
    #[error("unable to interface with property as object: {0}")]
    ObjectError(#[from] ObjectHelperError),
    #[error("invalid scope: {0}")]
    InvalidScope(#[from] FromObjectScopeError),
}

#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum PropertyType {
    Toggle = 1,
    Number = 2,
    Text = 3,
}

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

impl FromSql for PropertyType {
    fn from_sql(value: Value) -> Result<Self, FromSqlError> {
        match value {
            Value::Integer(v) if v == PropertyType::Toggle as i64 => Ok(PropertyType::Toggle),
            Value::Integer(v) if v == PropertyType::Number as i64 => Ok(PropertyType::Number),
            Value::Integer(v) if v == PropertyType::Text as i64 => Ok(PropertyType::Text),
            _ => Err(FromSqlError::UnexpectedType),
        }
    }
}

impl ToSql for PropertyType {
    fn to_sql(&self) -> Result<Value, ToSqlError> {
        Ok(Value::Integer(*self as i64))
    }
}

impl PropertyType {
    pub fn values() -> [PropertyType; 3] {
        [
            PropertyType::Toggle,
            PropertyType::Number,
            PropertyType::Text,
        ]
    }

    pub fn default_settings(&self) -> PropertySettings {
        match self {
            PropertyType::Toggle => PropertySettings::Toggle(ToggleSettings),
            PropertyType::Number => PropertySettings::Number(NumberSettings),
            PropertyType::Text => PropertySettings::Text(TextSettings),
        }
    }

    pub fn default_value(&self) -> EntityValue {
        match self {
            PropertyType::Toggle => EntityValue::Toggle(ToggleValue::False),
            PropertyType::Number => EntityValue::Number(0.0),
            PropertyType::Text => EntityValue::Text(String::new()),
        }
    }
}

#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum PropertyScope {
    Blueprint(Blueprint),
    Entity(Entity),
}

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

impl ToSql for PropertyScope {
    #[instrument(level = Level::TRACE)]
    fn to_sql(&self) -> Result<Value, ToSqlError> {
        match self {
            PropertyScope::Blueprint(blueprint) => blueprint.id.to_sql(),
            PropertyScope::Entity(entity) => entity.id.to_sql(),
        }
    }
}

impl PropertyScope {
    pub fn conn(&self) -> Result<Rc<Connection>, ExpiredConnectionError> {
        match self {
            PropertyScope::Blueprint(Blueprint { conn, .. }) => conn,
            PropertyScope::Entity(Entity { conn, .. }) => conn,
        }
        .upgrade()
        .ok_or(ExpiredConnectionError)
    }

    pub(crate) fn object(&self) -> Object {
        match self {
            PropertyScope::Blueprint(blueprint) => blueprint.to_object(),
            PropertyScope::Entity(entity) => entity.to_object(),
        }
    }

    pub(crate) fn id(&self) -> TypedId {
        *match self {
            PropertyScope::Blueprint(Blueprint { id, .. }) => id,
            PropertyScope::Entity(Entity { id, .. }) => id,
        }
    }
    pub fn from_object_scope(scope: ObjectScope) -> Result<PropertyScope, FromObjectScopeError> {
        match scope {
            ObjectScope::Property(scope) => Ok(scope),
            v => Err(FromObjectScopeError::InvalidScope("property", v)),
        }
    }

    pub fn to_object_scope(&self) -> ObjectScope {
        ObjectScope::Property(self.clone())
    }
}

define_object!(Property, PropertyFolder, PropertyScope, PropertyError);

impl Property {
    #[instrument(level = Level::INFO, skip(conn, name, information), fields(name = name.as_ref()))]
    pub fn new(
        conn: Rc<Connection>,
        scope: PropertyScope,
        parent_folder: Option<&PropertyFolder>,
        position: FolderPosition,
        name: impl AsRef<str>,
        information: impl AsRef<str>,
        property_type: PropertyType,
        property_settings: PropertySettings,
        default_value: EntityValue,
    ) -> Result<Property, PropertyError> {
        validate_string_not_blank(&name)?;

        validate_property_settings(
            PropertySettingsValidationData::PropertyType(property_type),
            &property_settings,
        )?;

        validate_entity_value(
            EntityValueValidationData::PropertyType(property_type),
            Some(&default_value),
        )?;

        let property = conn.transaction_rc(|conn| {
            let id = object_create(
                conn.clone(),
                ObjectType::Property,
                scope.to_object_scope(),
                parent_folder.map(|v| v.to_folder()),
                position,
                name,
                information,
            ).map_err(|e| DbError::TransactionError(e.to_string()))?;

            conn.execute(
                "INSERT INTO `properties`(`id`, `type`, `default_value`, `settings`) VALUES(?, ?, ?, ?)",
                (id, property_type, default_value, property_settings),
            )?;

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

        Ok(property)
    }

    #[instrument(level = Level::DEBUG)]
    pub fn r#type(&self) -> Result<PropertyType, PropertyError> {
        validate_object_exists(self.to_object())?;

        Ok(self.conn()?.query_one(
            "SELECT `type` FROM `properties` WHERE `id` = ?",
            self.id(),
            |row| row.get::<PropertyType>(0),
        )?)
    }

    #[instrument(level = Level::DEBUG)]
    pub fn settings(&self) -> Result<PropertySettings, PropertyError> {
        validate_object_exists(self.to_object())?;

        Ok(self.conn()?.query_one(
            "SELECT `settings` FROM `properties` WHERE `id` = ?",
            self.id(),
            |row| row.get::<PropertySettings>(0),
        )?)
    }

    // TODO set_settings

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

        Ok(self.conn()?.query_one(
            "SELECT `default_value` FROM `properties` WHERE `id` = ?",
            self.id(),
            |row| row.get::<EntityValue>(0),
        )?)
    }

    #[instrument(level = Level::INFO)]
    pub fn set_default_value(&self, new_default_value: &EntityValue) -> Result<(), PropertyError> {
        validate_object_exists(self.to_object())?;

        validate_entity_value(
            EntityValueValidationData::Property(self),
            Some(&new_default_value),
        )?;

        Ok(self.conn()?.execute(
            "UPDATE `properties` SET `default_value` = ? WHERE `id` = ?",
            (new_default_value, self.id()),
        )?)
    }
}