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),
)?)
}
#[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()),
)?)
}
}