use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use parking_lot::Mutex;
use tracing::error;
use crate::{prelude::*, properties::Property};
pub struct AnytypeCache {
spaces: Mutex<Option<Vec<Space>>>,
properties: Mutex<HashMap<String, HashMap<String, Arc<Property>>>>,
types: Mutex<HashMap<String, HashMap<String, Arc<Type>>>>,
enabled: Mutex<bool>,
}
impl Default for AnytypeCache {
fn default() -> Self {
Self {
enabled: Mutex::new(true),
spaces: Mutex::new(None),
properties: Mutex::new(HashMap::new()),
types: Mutex::new(HashMap::new()),
}
}
}
impl AnytypeCache {
#[must_use]
pub fn new_disabled() -> Self {
Self {
enabled: Mutex::new(false),
..Default::default()
}
}
pub fn clear(&self) {
self.clear_spaces();
self.clear_properties(None);
self.clear_types(None);
}
pub fn enable(&self) {
self.clear();
*self.enabled.lock() = true;
}
pub fn disable(&self) {
self.clear();
*self.enabled.lock() = false;
}
pub fn is_enabled(&self) -> bool {
*self.enabled.lock()
}
pub fn clear_space_items(&self, space_id: &str) {
self.clear_properties(Some(space_id));
self.clear_types(Some(space_id));
}
pub fn clear_spaces(&self) {
self.spaces.lock().take();
}
pub fn clear_properties(&self, space_id: Option<&str>) {
let mut properties = self.properties.lock();
if let Some(space_id) = space_id {
properties.remove(space_id);
} else {
properties.clear();
}
}
pub fn clear_types(&self, space_id: Option<&str>) {
let mut types = self.types.lock();
if let Some(space_id) = space_id {
types.remove(space_id);
} else {
types.clear();
}
}
pub(crate) fn spaces(&self) -> Option<Vec<Space>> {
if self.is_enabled() {
self.spaces.lock().clone()
} else {
None
}
}
pub(crate) fn set_spaces(&self, spaces: Vec<Space>) {
if self.is_enabled() {
*self.spaces.lock() = Some(spaces);
}
}
pub(crate) fn has_spaces(&self) -> bool {
self.is_enabled() && self.spaces.lock().is_some()
}
pub(crate) fn get_space(&self, space_id: &str) -> Option<Space> {
if self.is_enabled() {
self.spaces
.lock()
.as_ref()
.and_then(|spaces| spaces.iter().find(|space| space.id == space_id).cloned())
} else {
None
}
}
pub fn lookup_space_by_name(&self, name: impl AsRef<str>) -> Option<Space> {
let name = name.as_ref();
if self.is_enabled()
&& let Some(spaces) = self.spaces.lock().as_ref()
{
spaces.iter().find(|sp| sp.name == name).cloned()
} else {
None
}
}
pub(crate) fn properties_for_space(&self, space_id: &str) -> Option<Vec<Property>> {
if self.is_enabled() {
self.properties.lock().get(space_id).map(|map| {
let mut seen = HashSet::new();
map.values()
.filter(|arc| seen.insert(Arc::as_ptr(arc)))
.map(|arc| (**arc).clone())
.collect()
})
} else {
None
}
}
pub fn has_properties(&self, space_id: &str) -> bool {
self.is_enabled() && self.properties.lock().contains_key(space_id)
}
pub(crate) fn get_property(&self, space_id: &str, id_or_key: &str) -> Option<Arc<Property>> {
if self.is_enabled() {
self.properties
.lock()
.get(space_id)
.and_then(|properties| properties.get(id_or_key).cloned())
} else {
None
}
}
pub fn lookup_property(
&self,
space_id: &str,
text: impl AsRef<str>,
) -> Option<Vec<Arc<Property>>> {
if self.is_enabled()
&& let Some(map) = self.properties.lock().get(space_id)
{
let check = text.as_ref().trim().to_lowercase();
let mut seen = HashSet::new();
Some(
map.values()
.filter(|property| {
property.id == check
|| property.key == check
|| property.name.to_lowercase() == check
})
.filter(|arc| seen.insert(Arc::as_ptr(arc)))
.cloned()
.collect(),
)
} else {
None
}
}
pub fn lookup_property_by_key(
&self,
space_id: &str,
text: impl AsRef<str>,
) -> Option<Arc<Property>> {
if self.is_enabled()
&& let Some(map) = self.properties.lock().get(space_id)
{
let check = text.as_ref().trim().to_lowercase();
map.get(&check).cloned()
} else {
None
}
}
pub(crate) fn set_properties(&self, space_id: &str, properties: Vec<Property>) {
if !self.is_enabled() {
return;
}
let mut map = HashMap::with_capacity(properties.len() * 2);
for property in properties {
if map.contains_key(&property.id) {
error!(
space_id,
property_id = property.id.as_str(),
"duplicate property id in cache update"
);
}
let arc = Arc::new(property);
map.insert(arc.id.clone(), Arc::clone(&arc));
map.insert(arc.key.clone(), arc);
}
self.properties.lock().insert(space_id.to_string(), map);
}
pub(crate) fn set_property(&self, space_id: &str, property: Property) {
if self.is_enabled() && self.has_properties(space_id) {
let mut props_lock = self.properties.lock();
if let Some(space_props) = props_lock.get_mut(space_id) {
let arc = Arc::new(property);
space_props.insert(arc.id.clone(), Arc::clone(&arc));
space_props.insert(arc.key.clone(), arc);
}
}
}
pub(crate) fn delete_property(&self, space_id: &str, property_id: &str) {
if self.is_enabled() {
let mut props_lock = self.properties.lock();
if let Some(space_props) = props_lock.get_mut(space_id) {
if let Some(prop) = space_props.get(property_id).cloned() {
space_props.remove(&prop.id);
space_props.remove(&prop.key);
}
}
}
}
pub(crate) fn types_for_space(&self, space_id: &str) -> Option<Vec<Type>> {
if self.is_enabled() {
self.types.lock().get(space_id).map(|map| {
let mut seen = HashSet::new();
map.values()
.filter(|arc| seen.insert(Arc::as_ptr(arc)))
.map(|arc| (**arc).clone())
.collect()
})
} else {
None
}
}
pub fn lookup_types(&self, space_id: &str, text: impl AsRef<str>) -> Option<Vec<Arc<Type>>> {
if self.is_enabled()
&& let Some(map) = self.types.lock().get(space_id)
{
let check = text.as_ref().trim().to_lowercase();
let mut seen = HashSet::new();
Some(
map.values()
.filter(|type_| {
!type_.archived
&& (type_.id == check
|| type_.key == check
|| type_.name.as_deref().unwrap_or("").to_lowercase() == check
|| type_.plural_name.as_deref().unwrap_or("").to_lowercase()
== check)
})
.filter(|arc| seen.insert(Arc::as_ptr(arc)))
.cloned()
.collect(),
)
} else {
None
}
}
pub fn lookup_type_by_key(&self, space_id: &str, text: impl AsRef<str>) -> Option<Arc<Type>> {
if self.is_enabled()
&& let Some(map) = self.types.lock().get(space_id)
{
let check = text.as_ref().trim().to_lowercase();
map.get(&check).filter(|typ| !typ.archived).cloned()
} else {
None
}
}
pub(crate) fn has_types(&self, space_id: &str) -> bool {
self.is_enabled() && self.types.lock().contains_key(space_id)
}
pub(crate) fn get_type(&self, space_id: &str, id_or_key: &str) -> Option<Arc<Type>> {
if self.is_enabled() {
self.types
.lock()
.get(space_id)
.and_then(|types| types.get(id_or_key).cloned())
} else {
None
}
}
pub(crate) fn set_types(&self, space_id: &str, types: Vec<Type>) {
if !self.is_enabled() {
return;
}
let non_archived: Vec<_> = types.into_iter().filter(|typ| !typ.archived).collect();
let mut map = HashMap::with_capacity(non_archived.len() * 2);
for typ in non_archived {
let arc = Arc::new(typ);
map.insert(arc.id.clone(), Arc::clone(&arc));
map.insert(arc.key.clone(), arc);
}
self.types.lock().insert(space_id.to_string(), map);
}
pub(crate) fn set_type(&self, space_id: &str, typ: Type) {
if self.is_enabled() && self.has_types(space_id) {
let mut types_lock = self.types.lock();
if let Some(space_types) = types_lock.get_mut(space_id) {
let arc = Arc::new(typ);
space_types.insert(arc.id.clone(), Arc::clone(&arc));
space_types.insert(arc.key.clone(), arc);
}
}
}
pub(crate) fn delete_type(&self, space_id: &str, type_id: &str) {
if self.is_enabled() {
let mut types_lock = self.types.lock();
if let Some(space_types) = types_lock.get_mut(space_id) {
if let Some(typ) = space_types.get(type_id).cloned() {
space_types.remove(&typ.id);
space_types.remove(&typ.key);
}
}
}
}
}
impl AnytypeCache {
#[doc(hidden)]
pub fn num_spaces(&self) -> usize {
if self.is_enabled() {
self.spaces.lock().as_ref().map_or(0, Vec::len)
} else {
0
}
}
#[doc(hidden)]
pub fn num_properties(&self) -> usize {
if self.is_enabled() {
self.properties
.lock()
.values()
.map(|map| {
map.values().map(Arc::as_ptr).collect::<HashSet<_>>().len()
})
.sum()
} else {
0
}
}
#[doc(hidden)]
pub fn num_types(&self) -> usize {
if self.is_enabled() {
self.types
.lock()
.values()
.map(|map| {
map.values().map(Arc::as_ptr).collect::<HashSet<_>>().len()
})
.sum()
} else {
0
}
}
}
impl std::fmt::Debug for AnytypeCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let spaces_keys = self
.spaces
.lock()
.as_ref()
.map(|spaces| {
spaces
.iter()
.map(|space| space.name.as_str())
.collect::<Vec<&str>>()
.join(",")
})
.unwrap_or_default();
f.debug_struct("AnytypeCache")
.field("enabled", &self.is_enabled())
.field("spaces", &format!("keys: {}", &spaces_keys))
.field("properties", &format!("count: {}", self.num_properties()))
.field("types", &format!("count: {}", self.num_types()))
.finish()
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::AnytypeCache;
use crate::prelude::*;
fn sample_property(id: &str, key: &str) -> Property {
serde_json::from_value(json!({
"name": format!("prop {key}"),
"key": key,
"id": id,
"format": "text",
"tags": null
}))
.expect("property fixture")
}
fn sample_type(id: &str, key: &str) -> Type {
serde_json::from_value(json!({
"archived": false,
"icon": null,
"id": id,
"key": key,
"layout": "basic",
"name": format!("{key} type"),
"plural_name": format!("{key} types"),
"properties": [sample_property("prop-1", "title")]
}))
.expect("type fixture")
}
fn sample_space(id: &str, name: &str) -> Space {
serde_json::from_value(json!({
"id": id,
"name": name,
"object": "space",
"description": null,
"icon": null,
"gateway_url": null,
"network_id": null
}))
.expect("space fixture")
}
#[test]
fn test_cache_counts_and_clear() {
let cache = AnytypeCache::default();
cache.set_properties("space-a", vec![sample_property("p1", "title")]);
cache.set_properties("space-b", vec![sample_property("p2", "status")]);
cache.set_types("space-a", vec![sample_type("t1", "page")]);
cache.set_types("space-b", vec![sample_type("t2", "task")]);
cache.set_spaces(vec![
sample_space("s1", "Space One"),
sample_space("s2", "Space Two"),
]);
assert_eq!(cache.num_properties(), 2);
assert_eq!(cache.num_types(), 2);
assert_eq!(cache.num_spaces(), 2);
cache.clear_properties(Some("space-a"));
assert_eq!(cache.num_properties(), 1);
assert!(!cache.has_properties("space-a"));
assert!(cache.has_properties("space-b"));
cache.clear_types(None);
assert_eq!(cache.num_types(), 0);
cache.clear_spaces();
assert_eq!(cache.num_spaces(), 0);
}
#[test]
fn test_cache_disable_prevents_writes() {
let cache = AnytypeCache::default();
cache.disable();
cache.set_properties("space-a", vec![sample_property("p1", "title")]);
cache.set_types("space-a", vec![sample_type("t1", "page")]);
cache.set_spaces(vec![sample_space("s1", "Space One")]);
assert_eq!(cache.num_properties(), 0);
assert_eq!(cache.num_types(), 0);
assert_eq!(cache.num_spaces(), 0);
cache.enable();
cache.set_properties("space-a", vec![sample_property("p1", "title")]);
assert_eq!(cache.num_properties(), 1);
}
#[test]
fn test_cache_lookup_property_and_type() {
let cache = AnytypeCache::default();
cache.set_properties("space-a", vec![sample_property("p1", "status")]);
cache.set_types("space-a", vec![sample_type("t1", "page")]);
let prop = cache
.lookup_property_by_key("space-a", "status")
.expect("property lookup");
assert_eq!(prop.id, "p1");
let types = cache.lookup_types("space-a", "page").expect("type lookup");
assert_eq!(types.len(), 1);
assert_eq!(types[0].id, "t1");
}
}