Rust AppView - highly experimental!
at experiments 4.1 kB view raw
1use jacquard_api::app_bsky::richtext::facet::{Facet as FacetMain, FacetFeaturesItem}; 2use jacquard_common::types::blob::Blob; 3use jacquard_api::com_atproto::repo::strong_ref::StrongRef; 4use serde::{Deserialize as _, Deserializer}; 5 6// see https://deer.social/profile/did:plc:63y3oh7iakdueqhlj6trojbq/post/3ltuv4skhqs2h 7pub fn safe_string<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> { 8 let str = Option::<String>::deserialize(deserializer)?; 9 10 Ok(str.map(|s| s.replace('\u{0000}', ""))) 11} 12 13pub fn safe_string_required<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> { 14 let str = String::deserialize(deserializer)?; 15 Ok(str.replace('\u{0000}', "")) 16} 17 18pub fn strip_mime_params(mime_type: &str) -> String { 19 // Strip any parameters from the MIME type (e.g., "image/jpeg; quality=90" -> "image/jpeg") 20 mime_type.split(';').next().unwrap_or(mime_type).trim().to_string() 21} 22 23#[expect(clippy::single_option_map, reason = "Option::map() is clearer than and_then() for simple transformations")] 24pub fn blob_ref(blob: Option<&Blob>) -> Option<String> { 25 blob.map(|blob| blob.cid().to_string()) 26} 27 28/// Convert a Blob to CID bytes (32-byte digest) for database storage 29pub fn blob_to_cid_bytes(blob: Option<&Blob>) -> Option<Vec<u8>> { 30 blob.and_then(|blob| { 31 parakeet_db::utils::cid::blob_to_cid_bytes(Some(blob)) 32 }) 33} 34 35pub fn strongref_to_parts(strongref: Option<&StrongRef>) -> (Option<String>, Option<String>) { 36 parakeet_db::utils::cid::strongref_to_parts(strongref) 37} 38 39/// Convert a StrongRef to (URI, CID digest bytes) for pending linkage 40pub fn strongref_to_parts_with_digest( 41 strongref: Option<&StrongRef>, 42) -> (Option<String>, Option<Vec<u8>>) { 43 parakeet_db::utils::cid::strongref_to_parts_with_digest(strongref) 44} 45 46pub fn at_uri_is_by(uri: &str, did: &str) -> bool { 47 let split_aturi = uri.rsplitn(4, '/').collect::<Vec<_>>(); 48 49 did == split_aturi[2] 50} 51 52/// Extract the DID from an AT URI 53/// 54/// AT URIs have the format: `at://{did}/{collection}/{rkey}` 55/// This function extracts the DID component. 56pub fn extract_did_from_uri(uri: &str) -> Option<&str> { 57 // AT URI format: at://{did}/{collection}/{rkey} 58 // Split from the right: [rkey, collection, did, "at:/"] 59 let split_aturi = uri.rsplitn(4, '/').collect::<Vec<_>>(); 60 61 // Check that we have all 4 parts and it starts with "at:" 62 if split_aturi.len() == 4 && split_aturi[3].starts_with("at:") { 63 Some(split_aturi[2]) 64 } else { 65 None 66 } 67} 68 69pub fn extract_mentions_and_tags(from: &[FacetMain]) -> (Vec<String>, Vec<String>) { 70 let (mentions, tags) = from 71 .iter() 72 .flat_map(|v| { 73 v.features.iter().filter_map(|feature| { 74 match feature { 75 FacetFeaturesItem::Mention(mention) => Some((Some(mention.did.to_string()), None)), 76 FacetFeaturesItem::Tag(tag) => Some((None, Some(tag.tag.to_string()))), 77 _ => None, 78 } 79 }) 80 }) 81 .unzip::<_, _, Vec<_>, Vec<_>>(); 82 83 let mentions = mentions.into_iter().flatten().collect(); 84 let tags = tags.into_iter().flatten().collect(); 85 86 (mentions, tags) 87} 88 89pub fn merge_tags<T>(t1: Option<Vec<T>>, t2: Option<Vec<T>>) -> Vec<T> { 90 match (t1, t2) { 91 (Some(t1), None) => t1, 92 (None, Some(t2)) => t2, 93 (Some(mut t1), Some(t2)) => { 94 t1.extend(t2); 95 t1 96 } 97 _ => Vec::default(), 98 } 99} 100 101pub fn deserialize_optional_blob<'de, D>(deserializer: D) -> Result<Option<Blob<'static>>, D::Error> 102where 103 D: Deserializer<'de>, 104{ 105 use jacquard_common::IntoStatic; 106 let opt: Option<Blob<'_>> = Option::deserialize(deserializer)?; 107 Ok(opt.map(|b| b.into_static())) 108} 109 110pub fn deserialize_optional_strongref<'de, D>(deserializer: D) -> Result<Option<StrongRef<'static>>, D::Error> 111where 112 D: Deserializer<'de>, 113{ 114 use jacquard_common::IntoStatic; 115 let opt: Option<StrongRef<'_>> = Option::deserialize(deserializer)?; 116 Ok(opt.map(|s| s.into_static())) 117}