use std::sync::Arc;
use serde::{Deserialize, Deserializer, Serialize};
use snafu::prelude::*;
use crate::{
Result,
cache::AnytypeCache,
client::AnytypeClient,
error::{CacheDisabledSnafu, NotFoundSnafu, ValidationSnafu},
filters::{Query, QueryWithFilters},
http_client::{GetPaged, HttpClient},
prelude::*,
verify::{VerifyConfig, VerifyPolicy, resolve_verify, verify_available},
};
#[derive(
Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, strum::Display, strum::EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum TypeLayout {
#[default]
Basic,
Profile,
Action,
Note,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CreateTypeProperty {
pub format: PropertyFormat,
pub key: String,
pub name: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Type {
pub archived: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<Icon>,
pub id: String,
pub key: String,
#[serde(default)]
pub layout: ObjectLayout,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub plural_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_vec_properties_or_null")]
pub properties: Vec<Property>,
}
fn deserialize_vec_properties_or_null<'de, D>(deserializer: D) -> Result<Vec<Property>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<Vec<Property>>::deserialize(deserializer)?;
Ok(value.unwrap_or_default())
}
impl Type {
pub fn is_system_type(&self) -> bool {
matches!(self.key.as_str(), "page" | "note" | "task" | "bookmark")
}
pub fn display_name(&self) -> &str {
self.name.as_deref().unwrap_or(&self.key)
}
pub fn get_property_by_key(&self, property_key: &str) -> Option<&Property> {
self.properties.iter().find(|prop| prop.key == property_key)
}
}
#[derive(Debug, Deserialize)]
struct TypeResponse {
#[serde(rename = "type")]
type_: Type,
}
#[derive(Debug, Serialize)]
struct CreateTypeRequestBody {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
key: Option<String>,
plural_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<Icon>,
layout: TypeLayout,
#[serde(skip_serializing_if = "Vec::is_empty")]
properties: Vec<CreateTypeProperty>,
}
#[derive(Debug, Serialize, Default)]
struct UpdateTypeRequestBody {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
plural_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<Icon>,
#[serde(skip_serializing_if = "Option::is_none")]
layout: Option<TypeLayout>,
#[serde(skip_serializing_if = "Vec::is_empty")]
properties: Vec<CreateTypeProperty>,
}
#[derive(Debug)]
pub struct TypeRequest {
client: Arc<HttpClient>,
limits: ValidationLimits,
space_id: String,
type_id: String,
cache: Arc<AnytypeCache>,
}
impl TypeRequest {
pub(crate) fn new(
client: Arc<HttpClient>,
limits: ValidationLimits,
space_id: impl Into<String>,
type_id: impl Into<String>,
cache: Arc<AnytypeCache>,
) -> Self {
Self {
client,
limits,
space_id: space_id.into(),
type_id: type_id.into(),
cache,
}
}
pub async fn get(self) -> Result<Type> {
self.limits.validate_id(&self.space_id, "space_id")?;
self.limits.validate_id(&self.type_id, "type_id")?;
if self.cache.is_enabled() {
if let Some(typ) = self.cache.get_type(&self.space_id, &self.type_id) {
return Ok((*typ).clone());
}
if !self.cache.has_types(&self.space_id) {
prime_cache_types(&self.client, &self.cache, &self.space_id).await?;
if let Some(type_) = self.cache.get_type(&self.space_id, &self.type_id) {
return Ok((*type_).clone());
}
}
return NotFoundSnafu {
obj_type: "Type".to_string(),
key: self.type_id.clone(),
}
.fail();
}
let response: TypeResponse = self
.client
.get_request(
&format!("/v1/spaces/{}/types/{}", self.space_id, self.type_id),
QueryWithFilters::default(),
)
.await?;
Ok(response.type_)
}
pub async fn delete(self) -> Result<Type> {
self.limits.validate_id(&self.space_id, "space_id")?;
self.limits.validate_id(&self.type_id, "type_id")?;
let response: TypeResponse = self
.client
.delete_request(&format!(
"/v1/spaces/{}/types/{}",
self.space_id, self.type_id
))
.await?;
if self.cache.has_types(&self.space_id) {
self.cache.delete_type(&self.space_id, &self.type_id);
}
Ok(response.type_)
}
}
#[derive(Debug)]
pub struct NewTypeRequest {
client: Arc<HttpClient>,
limits: ValidationLimits,
space_id: String,
name: String,
key: Option<String>,
plural_name: String,
icon: Option<Icon>,
layout: TypeLayout,
properties: Vec<CreateTypeProperty>,
cache: Arc<AnytypeCache>,
verify_policy: VerifyPolicy,
verify_config: Option<VerifyConfig>,
}
impl NewTypeRequest {
pub(crate) fn new(
client: Arc<HttpClient>,
limits: ValidationLimits,
space_id: impl Into<String>,
name: String,
plural_name: String,
cache: Arc<AnytypeCache>,
verify_config: Option<VerifyConfig>,
) -> Self {
Self {
client,
limits,
space_id: space_id.into(),
name,
key: None,
plural_name,
icon: None,
layout: TypeLayout::Basic,
properties: Vec::new(),
cache,
verify_policy: VerifyPolicy::Default,
verify_config,
}
}
#[must_use]
pub fn plural_name(mut self, plural_name: impl Into<String>) -> Self {
self.plural_name = plural_name.into();
self
}
#[must_use]
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
#[must_use]
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
#[must_use]
pub fn layout(mut self, layout: TypeLayout) -> Self {
self.layout = layout;
self
}
#[must_use]
pub fn ensure_available(mut self) -> Self {
self.verify_policy = VerifyPolicy::Enabled;
self
}
#[must_use]
pub fn ensure_available_with(mut self, config: VerifyConfig) -> Self {
self.verify_policy = VerifyPolicy::Enabled;
self.verify_config = Some(config);
self
}
#[must_use]
pub fn no_verify(mut self) -> Self {
self.verify_policy = VerifyPolicy::Disabled;
self
}
#[must_use]
pub fn property(
mut self,
name: impl Into<String>,
key: impl Into<String>,
format: PropertyFormat,
) -> Self {
self.properties.push({
CreateTypeProperty {
name: name.into(),
key: key.into(),
format,
}
});
self
}
#[must_use]
pub fn properties(mut self, properties: impl IntoIterator<Item = CreateTypeProperty>) -> Self {
self.properties.extend(properties);
self
}
pub async fn create(self) -> Result<Type> {
self.limits.validate_id(&self.space_id, "space_id")?;
self.limits.validate_name(&self.name, "type name")?;
let request_body = CreateTypeRequestBody {
name: self.name,
key: self.key,
plural_name: self.plural_name,
icon: self.icon,
layout: self.layout,
properties: self.properties,
};
let response: TypeResponse = self
.client
.post_request(
&format!("/v1/spaces/{}/types", self.space_id),
&request_body,
QueryWithFilters::default(),
)
.await?;
if self.cache.has_types(&self.space_id) {
self.cache.set_type(&self.space_id, response.type_.clone());
}
let typ = response.type_;
if let Some(config) = resolve_verify(self.verify_policy, self.verify_config.as_ref()) {
return verify_available(&config, "Type", &typ.id, || async {
let response: TypeResponse = self
.client
.get_request(
&format!("/v1/spaces/{}/types/{}", self.space_id, typ.id),
QueryWithFilters::default(),
)
.await?;
Ok(response.type_)
})
.await;
}
Ok(typ)
}
}
#[derive(Debug)]
pub struct UpdateTypeRequest {
client: Arc<HttpClient>,
limits: ValidationLimits,
space_id: String,
type_id: String,
name: Option<String>,
key: Option<String>,
plural_name: Option<String>,
icon: Option<Icon>,
layout: Option<TypeLayout>,
properties: Vec<CreateTypeProperty>,
cache: Arc<AnytypeCache>,
verify_policy: VerifyPolicy,
verify_config: Option<VerifyConfig>,
}
impl UpdateTypeRequest {
pub(crate) fn new(
client: Arc<HttpClient>,
limits: ValidationLimits,
space_id: impl Into<String>,
type_id: impl Into<String>,
cache: Arc<AnytypeCache>,
verify_config: Option<VerifyConfig>,
) -> Self {
Self {
client,
limits,
space_id: space_id.into(),
type_id: type_id.into(),
name: None,
key: None,
plural_name: None,
icon: None,
layout: None,
properties: Vec::new(),
cache,
verify_policy: VerifyPolicy::Default,
verify_config,
}
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
#[must_use]
pub fn plural_name(mut self, plural_name: impl Into<String>) -> Self {
self.plural_name = Some(plural_name.into());
self
}
#[must_use]
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
#[must_use]
pub fn layout(mut self, layout: TypeLayout) -> Self {
self.layout = Some(layout);
self
}
#[must_use]
pub fn ensure_available(mut self) -> Self {
self.verify_policy = VerifyPolicy::Enabled;
self
}
#[must_use]
pub fn ensure_available_with(mut self, config: VerifyConfig) -> Self {
self.verify_policy = VerifyPolicy::Enabled;
self.verify_config = Some(config);
self
}
#[must_use]
pub fn no_verify(mut self) -> Self {
self.verify_policy = VerifyPolicy::Disabled;
self
}
#[must_use]
pub fn property(
mut self,
name: impl Into<String>,
key: impl Into<String>,
format: PropertyFormat,
) -> Self {
self.properties.push({
CreateTypeProperty {
name: name.into(),
key: key.into(),
format,
}
});
self
}
pub async fn update(self) -> Result<Type> {
self.limits.validate_id(&self.space_id, "space_id")?;
self.limits.validate_id(&self.type_id, "type_id")?;
ensure!(
self.name.is_some()
|| self.key.is_some()
|| self.plural_name.is_some()
|| self.icon.is_some()
|| self.layout.is_some()
|| !self.properties.is_empty(),
ValidationSnafu {
message: "update_type: must set at least one field to update".to_string(),
}
);
if let Some(ref name) = self.name {
self.limits.validate_name(name, "type")?;
}
let request_body = UpdateTypeRequestBody {
name: self.name,
key: self.key,
plural_name: self.plural_name,
icon: self.icon,
layout: self.layout,
properties: self.properties,
};
let response: TypeResponse = self
.client
.patch_request(
&format!("/v1/spaces/{}/types/{}", self.space_id, self.type_id),
&request_body,
)
.await?;
if self.cache.has_types(&self.space_id) {
self.cache.set_type(&self.space_id, response.type_.clone());
}
let typ = response.type_;
if let Some(config) = resolve_verify(self.verify_policy, self.verify_config.as_ref()) {
return verify_available(&config, "Type", &typ.id, || async {
let response: TypeResponse = self
.client
.get_request(
&format!("/v1/spaces/{}/types/{}", self.space_id, typ.id),
QueryWithFilters::default(),
)
.await?;
Ok(response.type_)
})
.await;
}
Ok(typ)
}
}
#[derive(Debug)]
pub struct ListTypesRequest {
client: Arc<HttpClient>,
limits: ValidationLimits,
space_id: String,
limit: Option<u32>,
offset: Option<u32>,
filters: Vec<Filter>,
cache: Arc<AnytypeCache>,
}
impl ListTypesRequest {
pub(crate) fn new(
client: Arc<HttpClient>,
limits: ValidationLimits,
space_id: impl Into<String>,
cache: Arc<AnytypeCache>,
) -> Self {
Self {
client,
limits,
space_id: space_id.into(),
limit: None,
offset: None,
filters: Vec::new(),
cache,
}
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn offset(mut self, offset: u32) -> Self {
self.offset = Some(offset);
self
}
#[must_use]
pub fn filter(mut self, filter: Filter) -> Self {
self.filters.push(filter);
self
}
#[must_use]
pub fn filters(mut self, filters: impl IntoIterator<Item = Filter>) -> Self {
self.filters.extend(filters);
self
}
pub async fn list(self) -> Result<PagedResult<Type>> {
self.limits.validate_id(&self.space_id, "space_id")?;
if self.cache.is_enabled()
&& self.limit.is_none()
&& self.offset.unwrap_or_default() == 0
&& self.filters.is_empty()
{
if !self.cache.has_types(&self.space_id) {
prime_cache_types(&self.client, &self.cache, &self.space_id).await?;
}
return Ok(PagedResult::from_items(
self.cache
.types_for_space(&self.space_id)
.unwrap_or_default(),
));
}
let query = Query::default()
.set_limit_opt(self.limit)
.set_offset_opt(self.offset)
.add_filters(&self.filters);
self.client
.get_request_paged(&format!("/v1/spaces/{}/types", self.space_id), query)
.await
}
}
async fn prime_cache_types(
client: &Arc<HttpClient>,
cache: &Arc<AnytypeCache>,
space_id: &str,
) -> Result<()> {
let types: Vec<Type> = client
.get_request_paged(
&format!("/v1/spaces/{space_id}/types"),
QueryWithFilters::default(),
)
.await?
.collect_all()
.await?
.into_iter()
.filter(|typ: &Type| !typ.archived)
.collect();
cache.set_types(space_id, types);
Ok(())
}
impl AnytypeClient {
pub fn get_type(&self, space_id: impl Into<String>, type_id: impl Into<String>) -> TypeRequest {
TypeRequest::new(
self.client.clone(),
self.config.limits.clone(),
space_id,
type_id,
self.cache.clone(),
)
}
pub fn new_type(&self, space_id: impl Into<String>, name: impl Into<String>) -> NewTypeRequest {
let name = name.into();
let plural_name = format!("{}s", &name);
NewTypeRequest::new(
self.client.clone(),
self.config.limits.clone(),
space_id,
name,
plural_name,
self.cache.clone(),
self.config.verify.clone(),
)
}
pub fn update_type(
&self,
space_id: impl Into<String>,
type_id: impl Into<String>,
) -> UpdateTypeRequest {
UpdateTypeRequest::new(
self.client.clone(),
self.config.limits.clone(),
space_id,
type_id,
self.cache.clone(),
self.config.verify.clone(),
)
}
pub fn types(&self, space_id: impl Into<String>) -> ListTypesRequest {
ListTypesRequest::new(
self.client.clone(),
self.config.limits.clone(),
space_id,
self.cache.clone(),
)
}
pub async fn lookup_types(&self, space_id: &str, text: impl AsRef<str>) -> Result<Vec<Type>> {
ensure!(self.cache.is_enabled(), CacheDisabledSnafu);
if !self.cache.has_types(space_id) {
prime_cache_types(&self.client, &self.cache, space_id).await?;
}
match self.cache.lookup_types(space_id, text.as_ref()) {
Some(types) if !types.is_empty() => {
Ok(types.into_iter().map(|arc| (*arc).clone()).collect())
}
_ => NotFoundSnafu {
obj_type: "Type".to_string(),
key: text.as_ref().to_string(),
}
.fail(),
}
}
pub async fn lookup_type_by_key(&self, space_id: &str, text: impl AsRef<str>) -> Result<Type> {
ensure!(self.cache.is_enabled(), CacheDisabledSnafu);
if !self.cache.has_types(space_id) {
prime_cache_types(&self.client, &self.cache, space_id).await?;
}
self.cache
.lookup_type_by_key(space_id, text.as_ref())
.map_or_else(
|| {
NotFoundSnafu {
obj_type: "Type".to_string(),
key: text.as_ref().to_string(),
}
.fail()
},
|typ| Ok((*typ).clone()),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_type_layout_default() {
let layout = TypeLayout::default();
assert_eq!(layout, TypeLayout::Basic);
}
#[test]
fn test_type_layout_display() {
assert_eq!(TypeLayout::Basic.to_string(), "basic");
assert_eq!(TypeLayout::Note.to_string(), "note");
assert_eq!(TypeLayout::Action.to_string(), "action");
}
#[test]
fn test_type_layout_from_string() {
use std::str::FromStr;
assert_eq!(TypeLayout::from_str("basic").unwrap(), TypeLayout::Basic);
assert_eq!(TypeLayout::from_str("note").unwrap(), TypeLayout::Note);
}
#[test]
fn test_type_is_system_type() {
let page_type = Type {
archived: false,
id: "id".to_string(),
key: "page".to_string(),
name: Some("Page".to_string()),
plural_name: None,
icon: None,
layout: ObjectLayout::Basic,
properties: vec![],
};
assert!(page_type.is_system_type());
let custom_type = Type {
archived: false,
id: "id".to_string(),
key: "project".to_string(),
name: Some("Project".to_string()),
plural_name: None,
icon: None,
layout: ObjectLayout::Basic,
properties: vec![],
};
assert!(!custom_type.is_system_type());
}
#[test]
fn test_type_display_name() {
let with_name = Type {
archived: false,
id: "id".to_string(),
key: "page".to_string(),
name: Some("Page".to_string()),
plural_name: None,
icon: None,
layout: ObjectLayout::Basic,
properties: vec![],
};
assert_eq!(with_name.display_name(), "Page");
let without_name = Type {
archived: false,
id: "id".to_string(),
key: "custom_type".to_string(),
name: None,
plural_name: None,
icon: None,
layout: ObjectLayout::Basic,
properties: vec![],
};
assert_eq!(without_name.display_name(), "custom_type");
}
}