use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const OBJECT_MANIFEST_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DocumentConversion {
pub source_hash: String,
pub source_extension: String,
pub pdf_hash: String,
pub recipe: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum AttachmentFileType {
TextLocal,
ImageUpload,
DocumentUpload,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ObjectFileContext {
pub file_type: AttachmentFileType,
pub image_tone: Option<String>,
pub file_brief: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ObjectRemote {
pub file_uri: String,
pub file_name: String,
pub mime_type: String,
pub uploaded_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub validated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReverseImageSearchCache {
pub imgbb_url: String,
pub google_lens_url: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ObjectManifest {
pub schema: u32,
pub file_context: ObjectFileContext,
pub reverse_image_search: Option<ReverseImageSearchCache>,
#[serde(deserialize_with = "deserialize_object_remotes")]
pub object_remotes: BTreeMap<String, ObjectRemote>,
}
impl ObjectManifest {
pub fn new(file_context: ObjectFileContext) -> Self {
Self {
schema: OBJECT_MANIFEST_SCHEMA_VERSION,
file_context,
reverse_image_search: None,
object_remotes: BTreeMap::new(),
}
}
pub fn validate(&self) -> std::result::Result<(), String> {
if self.schema != OBJECT_MANIFEST_SCHEMA_VERSION {
return Err(format!(
"expected object manifest schema {OBJECT_MANIFEST_SCHEMA_VERSION}"
));
}
if self.reverse_image_search.as_ref().is_some_and(|cache| {
cache.imgbb_url.trim().is_empty() || cache.google_lens_url.trim().is_empty()
}) {
return Err("reverse image search cache URLs must not be empty".to_string());
}
for remote_id in self.object_remotes.keys() {
validate_object_remote_id(remote_id)?;
}
Ok(())
}
}
fn validate_object_remote_id(remote_id: &str) -> std::result::Result<(), String> {
if remote_id.len() == 64
&& remote_id
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
Ok(())
} else {
Err("object remote ID must be exactly 64 lowercase hexadecimal characters".to_string())
}
}
fn deserialize_object_remotes<'de, D>(
deserializer: D,
) -> std::result::Result<BTreeMap<String, ObjectRemote>, D::Error>
where
D: serde::Deserializer<'de>,
{
let remotes = BTreeMap::<String, ObjectRemote>::deserialize(deserializer)?;
for remote_id in remotes.keys() {
validate_object_remote_id(remote_id).map_err(serde::de::Error::custom)?;
}
Ok(remotes)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredImage {
pub hash: String,
pub path: String,
#[serde(default)]
pub tone: Option<String>,
}