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(())
}
}