use std::any::TypeId;
use std::marker::PhantomData;
use dex_protocol::dex::{AttributeSyncConfig, AttributeWrite, IndexConfig, value};
use crate::{Context, HandlerError, HandlerResult, Value};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AttributeMapLoad {
pub(crate) name: String,
pub(crate) instance: Option<String>,
}
pub struct AttributeMapInstance<T> {
attribute_map: AttributeMap<T>,
instance: String,
}
impl<T> AttributeMapInstance<T> {
pub fn attribute_map(&self) -> &AttributeMap<T> {
&self.attribute_map
}
pub fn instance(&self) -> &str {
&self.instance
}
}
impl<T> Clone for AttributeMapInstance<T> {
fn clone(&self) -> Self {
Self {
attribute_map: self.attribute_map.clone(),
instance: self.instance.clone(),
}
}
}
pub struct Attribute<T> {
name: String,
index: Option<AttributeIndex>,
sync_to_attribute_store: bool,
is_hard_delete_on_null: bool,
numeric_or_boolean_type_name: Option<&'static str>,
marker: PhantomData<fn() -> T>,
}
impl<T> Attribute<T> {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
index: None,
sync_to_attribute_store: false,
is_hard_delete_on_null: false,
numeric_or_boolean_type_name: None,
marker: PhantomData,
}
}
pub fn indexed(mut self, index: AttributeIndex) -> Self {
self.index = Some(index);
self
}
#[must_use]
pub fn sync_to_attribute_store(mut self) -> Self {
self.sync_to_attribute_store = true;
self
}
#[must_use]
pub fn hard_delete_on_null(mut self) -> Self
where
T: 'static,
{
self.is_hard_delete_on_null = true;
self.numeric_or_boolean_type_name = numeric_or_boolean_type_name::<T>();
self
}
pub fn get(&self, context: &Context) -> HandlerResult<Option<T>>
where
T: Value,
{
context.get_attribute(self)
}
pub fn get_required(&self, context: &Context) -> HandlerResult<T>
where
T: Value,
{
self.get(context)?.ok_or_else(|| {
HandlerError::new(
"dex_sdk::HandlerError",
format!("attribute {} is missing", self.name),
)
})
}
pub fn set(&self, context: &mut Context, value: T) -> HandlerResult<()>
where
T: Value,
{
context.set_attribute(self, value)
}
pub fn delete(&self, context: &mut Context) -> HandlerResult<()> {
context.delete_attribute(self)
}
pub fn lock(&self) -> AttributeLock {
AttributeLock {
attribute: self.name.clone(),
instance: None,
}
}
pub(crate) fn name(&self) -> &str {
&self.name
}
pub(crate) fn index(&self) -> Option<&AttributeIndex> {
self.index.as_ref()
}
pub(crate) fn sync_config(&self) -> Option<AttributeSyncConfig> {
self.sync_to_attribute_store
.then_some(AttributeSyncConfig { enabled: true })
}
pub(crate) fn is_sync_to_attribute_store(&self) -> bool {
self.sync_to_attribute_store
}
pub(crate) fn is_hard_delete_on_null(&self) -> bool {
self.is_hard_delete_on_null
}
pub(crate) fn numeric_or_boolean_type_name(&self) -> Option<&'static str> {
self.numeric_or_boolean_type_name
}
pub(crate) fn write_settings(&self) -> AttributeWriteSettings {
AttributeWriteSettings {
index_config: self.index().map(|index| index.proto_config(false)),
sync_config: self.sync_config(),
is_hard_delete_on_null: self.is_hard_delete_on_null,
}
}
}
impl<T> Clone for Attribute<T> {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
index: self.index.clone(),
sync_to_attribute_store: self.sync_to_attribute_store,
is_hard_delete_on_null: self.is_hard_delete_on_null,
numeric_or_boolean_type_name: self.numeric_or_boolean_type_name,
marker: PhantomData,
}
}
}
pub struct AttributeMap<T> {
name: String,
index: Option<AttributeIndex>,
sync_to_attribute_store: bool,
is_hard_delete_on_null: bool,
numeric_or_boolean_type_name: Option<&'static str>,
marker: PhantomData<fn() -> T>,
}
impl<T> AttributeMap<T> {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
index: None,
sync_to_attribute_store: false,
is_hard_delete_on_null: false,
numeric_or_boolean_type_name: None,
marker: PhantomData,
}
}
pub fn indexed(mut self, index: AttributeIndex) -> Self {
self.index = Some(index);
self
}
#[must_use]
pub fn sync_to_attribute_store(mut self) -> Self {
self.sync_to_attribute_store = true;
self
}
#[must_use]
pub fn hard_delete_on_null(mut self) -> Self
where
T: 'static,
{
self.is_hard_delete_on_null = true;
self.numeric_or_boolean_type_name = numeric_or_boolean_type_name::<T>();
self
}
pub fn load(&self, instance: impl Into<String>) -> AttributeMapLoad {
AttributeMapLoad {
name: self.name.clone(),
instance: Some(instance.into()),
}
}
pub fn instance(&self, instance: impl Into<String>) -> AttributeMapInstance<T> {
AttributeMapInstance {
attribute_map: self.clone(),
instance: instance.into(),
}
}
pub fn get(&self, context: &Context, instance: &str) -> HandlerResult<Option<T>>
where
T: Value,
{
context.get_attribute_map(self, instance)
}
pub fn get_required(&self, context: &Context, instance: &str) -> HandlerResult<T>
where
T: Value,
{
self.get(context, instance)?.ok_or_else(|| {
HandlerError::new(
"dex_sdk::HandlerError",
format!("attribute {}[{instance}] is missing", self.name),
)
})
}
pub fn set(&self, context: &mut Context, instance: &str, value: T) -> HandlerResult<()>
where
T: Value,
{
context.set_attribute_map(self, instance, value)
}
pub fn delete(&self, context: &mut Context, instance: &str) -> HandlerResult<()> {
context.delete_attribute_map(self, instance)
}
pub fn map_size(&self, context: &Context) -> HandlerResult<usize> {
Ok(self.all_instance_keys(context)?.len())
}
pub fn all_instance_keys(&self, context: &Context) -> HandlerResult<Vec<String>> {
context.attribute_map_keys(self)
}
pub fn lock(&self, instance: impl Into<String>) -> AttributeLock {
let instance = instance.into();
crate::registry::assert_map_instance(&instance);
AttributeLock {
attribute: self.name.clone(),
instance: Some(instance),
}
}
pub(crate) fn name(&self) -> &str {
&self.name
}
pub(crate) fn index(&self) -> Option<&AttributeIndex> {
self.index.as_ref()
}
pub(crate) fn sync_config(&self) -> Option<AttributeSyncConfig> {
self.sync_to_attribute_store
.then_some(AttributeSyncConfig { enabled: true })
}
pub(crate) fn is_sync_to_attribute_store(&self) -> bool {
self.sync_to_attribute_store
}
pub(crate) fn is_hard_delete_on_null(&self) -> bool {
self.is_hard_delete_on_null
}
pub(crate) fn numeric_or_boolean_type_name(&self) -> Option<&'static str> {
self.numeric_or_boolean_type_name
}
pub(crate) fn write_settings(&self) -> AttributeWriteSettings {
AttributeWriteSettings {
index_config: self.index().map(|index| index.proto_config(true)),
sync_config: self.sync_config(),
is_hard_delete_on_null: self.is_hard_delete_on_null,
}
}
}
impl<T> Clone for AttributeMap<T> {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
index: self.index.clone(),
sync_to_attribute_store: self.sync_to_attribute_store,
is_hard_delete_on_null: self.is_hard_delete_on_null,
numeric_or_boolean_type_name: self.numeric_or_boolean_type_name,
marker: PhantomData,
}
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct AttributeWriteSettings {
pub(crate) index_config: Option<IndexConfig>,
pub(crate) sync_config: Option<AttributeSyncConfig>,
pub(crate) is_hard_delete_on_null: bool,
}
impl AttributeWriteSettings {
pub(crate) fn attribute_write(
self,
key: String,
value: dex_protocol::dex::Value,
) -> AttributeWrite {
AttributeWrite {
key,
value: Some(value),
index_config: self.index_config,
sync_config: self.sync_config,
is_hard_delete_on_null: self.is_hard_delete_on_null,
}
}
}
pub(crate) fn validate_hard_delete_attribute_write(write: &AttributeWrite) -> Result<(), String> {
if !write.is_hard_delete_on_null {
return Ok(());
}
let key = &write.key;
if write
.index_config
.as_ref()
.is_some_and(|config| config.enable)
{
return Err(format!(
"hard-delete Attribute {key} cannot be combined with an index"
));
}
if write
.sync_config
.as_ref()
.is_some_and(|config| config.enabled)
{
return Err(format!(
"hard-delete Attribute {key} cannot be combined with sync_to_attribute_store"
));
}
match write.value.as_ref().and_then(|value| value.kind.as_ref()) {
Some(value::Kind::IntValue(_))
| Some(value::Kind::DoubleValue(_))
| Some(value::Kind::BoolValue(_)) => Err(format!(
"hard-delete Attribute {key} supports only string and object values; numeric and \
Boolean values are rejected"
)),
_ => Ok(()),
}
}
pub(crate) fn require_registered_hard_delete_setting(
attribute_name: &str,
is_registered_hard_delete_on_null: bool,
is_write_hard_delete_on_null: bool,
) -> Result<(), String> {
if is_registered_hard_delete_on_null == is_write_hard_delete_on_null {
return Ok(());
}
Err(format!(
"Attribute {attribute_name} hard_delete_on_null setting does not match its registered \
definition"
))
}
fn numeric_or_boolean_type_name<T: 'static>() -> Option<&'static str> {
macro_rules! scalar_type_ids {
($($scalar:ty),* $(,)?) => {
[$(TypeId::of::<$scalar>(), TypeId::of::<Option<$scalar>>()),*]
};
}
let scalar_type_ids = scalar_type_ids!(
i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool,
);
scalar_type_ids
.contains(&TypeId::of::<T>())
.then(std::any::type_name::<T>)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AttributeIndex {
kind: AttributeIndexKind,
key: Option<String>,
}
impl AttributeIndex {
fn new(kind: AttributeIndexKind) -> Self {
Self { kind, key: None }
}
pub fn keyword() -> Self {
Self::new(AttributeIndexKind::Keyword)
}
pub fn full_text() -> Self {
Self::new(AttributeIndexKind::FullText)
}
pub fn keyword_array() -> Self {
Self::new(AttributeIndexKind::KeywordArray)
}
pub fn int() -> Self {
Self::new(AttributeIndexKind::Int)
}
pub fn double() -> Self {
Self::new(AttributeIndexKind::Double)
}
pub fn bool() -> Self {
Self::new(AttributeIndexKind::Bool)
}
pub fn date_time() -> Self {
Self::new(AttributeIndexKind::DateTime)
}
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum AttributeIndexKind {
Keyword,
FullText,
KeywordArray,
Int,
Double,
Bool,
DateTime,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AttributeLock {
attribute: String,
instance: Option<String>,
}
impl AttributeIndex {
pub(crate) fn proto_config(&self, dynamic: bool) -> IndexConfig {
IndexConfig {
enable: true,
r#type: self.kind.proto_value(),
index_key: self
.key
.clone()
.or_else(|| dynamic.then(String::new))
.unwrap_or_default(),
}
}
pub(crate) fn key(&self) -> Option<&str> {
self.key.as_deref()
}
pub(crate) fn proto_value(&self) -> i32 {
self.kind.proto_value()
}
}
impl AttributeIndexKind {
pub(crate) fn proto_value(self) -> i32 {
use dex_protocol::dex::IndexType;
match self {
Self::Keyword => IndexType::Keyword.into(),
Self::FullText => IndexType::Text.into(),
Self::KeywordArray => IndexType::KeywordArray.into(),
Self::Int => IndexType::Int.into(),
Self::Double => IndexType::Double.into(),
Self::Bool => IndexType::Bool.into(),
Self::DateTime => IndexType::Datetime.into(),
}
}
}
impl AttributeLock {
pub(crate) fn attribute_name(&self) -> &str {
&self.attribute
}
pub(crate) fn instance(&self) -> Option<&str> {
self.instance.as_deref()
}
pub(crate) fn physical_name(&self) -> String {
match self.instance.as_deref() {
Some(instance) => crate::registry::physical_name(&self.attribute, instance),
None => self.attribute.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::{Attribute, AttributeMap};
#[test]
fn sync_configuration_is_opt_in_and_survives_clone() {
let plain = Attribute::<String>::new("plain");
let synced = Attribute::<String>::new("synced").sync_to_attribute_store();
let synced_map = AttributeMap::<String>::new("synced_map").sync_to_attribute_store();
assert!(plain.sync_config().is_none());
assert_eq!(
synced.sync_config().map(|config| config.enabled),
Some(true)
);
assert_eq!(
synced_map
.clone()
.sync_config()
.map(|config| config.enabled),
Some(true)
);
}
}