pubky_app_specs/
traits.rs1use crate::common::timestamp;
2use base32::{decode, encode, Alphabet};
3use blake3::Hasher;
4use serde::de::DeserializeOwned;
5
6pub trait TimestampId {
7 fn create_id(&self) -> String {
9 let now = timestamp();
11
12 let bytes = now.to_be_bytes();
14
15 encode(Alphabet::Crockford, &bytes)
17 }
18
19 fn validate_id(&self, id: &str) -> Result<(), String> {
22 if id.len() != 13 {
24 return Err("Validation Error: Invalid ID length: must be 13 characters".into());
25 }
26
27 let decoded_bytes =
29 decode(Alphabet::Crockford, id).ok_or("Failed to decode Crockford Base32 ID")?;
30
31 if decoded_bytes.len() != 8 {
32 return Err("Validation Error: Invalid ID length after decoding".into());
33 }
34
35 let timestamp_micros = i64::from_be_bytes(decoded_bytes.try_into().unwrap());
37
38 let now_micros = timestamp();
40
41 let oct_first_2024_micros = 1727740800000000; let max_future_micros = now_micros + 2 * 60 * 60 * 1_000_000;
46
47 if timestamp_micros < oct_first_2024_micros {
49 return Err(
50 "Validation Error: Invalid ID, timestamp must be after October 1st, 2024".into(),
51 );
52 }
53
54 if timestamp_micros > max_future_micros {
56 return Err("Validation Error: Invalid ID, timestamp is too far in the future".into());
57 }
58
59 Ok(())
60 }
61}
62
63pub trait HashId {
65 fn get_id_data(&self) -> String;
66
67 fn create_id(&self) -> String {
80 let data = self.get_id_data();
81
82 let mut hasher = Hasher::new();
84 hasher.update(data.as_bytes());
85 let blake3_hash = hasher.finalize();
86
87 let half_hash_length = blake3_hash.as_bytes().len() / 2;
89 let half_hash = &blake3_hash.as_bytes()[..half_hash_length];
90
91 encode(Alphabet::Crockford, half_hash)
93 }
94
95 fn validate_id(&self, id: &str) -> Result<(), String> {
97 let generated_id = self.create_id();
98 if generated_id != id {
99 return Err(format!("Invalid ID: expected {generated_id}, found {id}"));
100 }
101 Ok(())
102 }
103}
104
105pub trait Validatable: Sized + DeserializeOwned {
106 fn try_from(blob: &[u8], id: &str) -> Result<Self, String> {
107 let mut instance: Self = serde_json::from_slice(blob).map_err(|e| e.to_string())?;
108 instance = instance.sanitize();
109 instance.validate(Some(id))?;
110 Ok(instance)
111 }
112
113 fn validate(&self, id: Option<&str>) -> Result<(), String>;
114
115 fn sanitize(self) -> Self {
116 self
117 }
118}
119
120pub trait HasPath {
121 const PATH_SEGMENT: &'static str;
122 fn create_path() -> String;
123}
124
125pub trait HasIdPath {
126 const PATH_SEGMENT: &'static str;
127 fn create_path(id: &str) -> String;
128}
129
130#[cfg(target_arch = "wasm32")]
131use serde::Serialize;
132#[cfg(target_arch = "wasm32")]
133use serde_wasm_bindgen::{from_value, to_value};
134#[cfg(target_arch = "wasm32")]
135use wasm_bindgen::JsValue;
136
137#[cfg(target_arch = "wasm32")]
139pub trait Json: Serialize + DeserializeOwned + Validatable {
140 fn export_json(&self) -> Result<JsValue, String> {
141 to_value(&self).map_err(|e| format!("JSON serialization error: {}", e))
142 }
143
144 fn import_json(js_value: &JsValue) -> Result<Self, String> {
145 let object: Self =
146 from_value(js_value.clone()).map_err(|e| format!("Error parsing js object: {}", e))?;
147 let object = object.sanitize();
148 object.validate(None)?;
149 Ok(object)
150 }
151}