Skip to main content

ankurah_proto/
id.rs

1use base64::{engine::general_purpose, Engine as _};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use ulid::Ulid;
5
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::*;
8
9use crate::error::{DecodeError, IdParseError};
10// TODO - split out the different id types. Presently there's a lot of not-entities that are using this type for their ID
11#[derive(PartialEq, Eq, Hash, Clone, Copy, Ord, PartialOrd)]
12#[cfg_attr(feature = "wasm", wasm_bindgen)]
13#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
14pub struct EntityId(pub(crate) Ulid);
15
16impl EntityId {
17    pub fn new() -> Self { EntityId(Ulid::new()) }
18
19    pub fn from_bytes(bytes: [u8; 16]) -> Self { EntityId(Ulid::from_bytes(bytes)) }
20
21    pub fn to_bytes(&self) -> [u8; 16] { self.0.to_bytes() }
22
23    pub fn from_base64<T: AsRef<[u8]>>(input: T) -> Result<Self, DecodeError> {
24        let decoded = general_purpose::URL_SAFE_NO_PAD.decode(input).map_err(DecodeError::InvalidBase64)?;
25        let bytes: [u8; 16] = decoded[..].try_into().map_err(|_| DecodeError::InvalidLength)?;
26
27        Ok(EntityId(Ulid::from_bytes(bytes)))
28    }
29
30    pub fn to_base64(&self) -> String { general_purpose::URL_SAFE_NO_PAD.encode(self.0.to_bytes()) }
31
32    pub fn to_base64_short(&self) -> String {
33        // take the last 6 characters of the base64 encoded string
34        let value = self.to_base64();
35        value[value.len() - 6..].to_string()
36    }
37
38    pub fn to_ulid(&self) -> Ulid { self.0 }
39    pub fn from_ulid(ulid: Ulid) -> Self { EntityId(ulid) }
40}
41
42// Methods exported to both WASM and UniFFI
43#[cfg_attr(feature = "wasm", wasm_bindgen)]
44#[cfg_attr(feature = "uniffi", uniffi::export)]
45impl EntityId {
46    #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = toString))]
47    pub fn to_string(&self) -> String { self.to_base64() }
48}
49
50// WASM-only methods
51#[cfg(feature = "wasm")]
52#[wasm_bindgen]
53impl EntityId {
54    #[wasm_bindgen(js_name = to_base64)]
55    pub fn to_base64_js(&self) -> String { general_purpose::URL_SAFE_NO_PAD.encode(self.0.to_bytes()) }
56
57    #[wasm_bindgen(js_name = from_base64)]
58    pub fn from_base64_js(s: &str) -> Result<Self, JsValue> { Self::from_base64(s).map_err(|e| JsValue::from_str(&e.to_string())) }
59
60    #[wasm_bindgen]
61    pub fn equals(&self, other: &EntityId) -> bool { self.0 == other.0 }
62}
63
64// UniFFI-only methods
65#[cfg(feature = "uniffi")]
66#[uniffi::export]
67impl EntityId {
68    /// Parse an EntityId from a base64 string
69    #[uniffi::constructor(name = "fromBase64")]
70    pub fn from_base64_uniffi(s: String) -> Result<Self, IdParseError> { Self::from_base64(s).map_err(|e| e.into()) }
71
72    /// Compare two EntityIds for equality
73    #[uniffi::method(name = "equals")]
74    pub fn equals_uniffi(&self, other: &EntityId) -> bool { self.0 == other.0 }
75}
76
77impl fmt::Display for EntityId {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
79        if f.alternate() {
80            write!(f, "{}", self.to_base64_short())
81        } else {
82            write!(f, "{}", self.to_base64())
83        }
84    }
85}
86impl std::fmt::Debug for EntityId {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.to_base64()) }
88}
89
90impl TryFrom<&str> for EntityId {
91    type Error = DecodeError;
92    fn try_from(id: &str) -> Result<Self, Self::Error> { Self::from_base64(id) }
93}
94
95impl TryFrom<String> for EntityId {
96    type Error = DecodeError;
97    fn try_from(id: String) -> Result<Self, Self::Error> { Self::try_from(id.as_str()) }
98}
99
100impl TryFrom<&String> for EntityId {
101    type Error = DecodeError;
102    fn try_from(id: &String) -> Result<Self, Self::Error> { Self::try_from(id.as_str()) }
103}
104
105impl std::str::FromStr for EntityId {
106    type Err = DecodeError;
107    fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_base64(s) }
108}
109
110impl From<EntityId> for String {
111    fn from(id: EntityId) -> String { id.to_base64() }
112}
113
114impl From<&EntityId> for String {
115    fn from(id: &EntityId) -> String { id.to_base64() }
116}
117
118impl TryInto<EntityId> for Vec<u8> {
119    type Error = DecodeError;
120    fn try_into(self) -> Result<EntityId, Self::Error> {
121        let bytes: [u8; 16] = self.try_into().map_err(|_| DecodeError::InvalidLength)?;
122        Ok(EntityId(Ulid::from_bytes(bytes)))
123    }
124}
125
126impl From<EntityId> for Ulid {
127    fn from(id: EntityId) -> Self { id.0 }
128}
129
130impl Default for EntityId {
131    fn default() -> Self { Self::new() }
132}
133
134impl Serialize for EntityId {
135    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
136    where S: serde::Serializer {
137        if serializer.is_human_readable() {
138            // Use base64 for human-readable formats like JSON
139            serializer.serialize_str(&self.to_base64())
140        } else {
141            // Use raw bytes as a fixed-size array for binary formats like bincode
142            self.to_bytes().serialize(serializer)
143        }
144    }
145}
146
147impl<'de> Deserialize<'de> for EntityId {
148    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
149    where D: serde::Deserializer<'de> {
150        if deserializer.is_human_readable() {
151            // Deserialize from base64 string for human-readable formats
152            let s = String::deserialize(deserializer)?;
153            EntityId::from_base64(s).map_err(serde::de::Error::custom)
154        } else {
155            // Deserialize from raw bytes as a fixed-size array for binary formats
156            let bytes = <[u8; 16]>::deserialize(deserializer)?;
157            Ok(EntityId::from_bytes(bytes))
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_entity_id_json_serialization() {
168        let id = EntityId::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
169        let json = serde_json::to_string(&id).unwrap();
170        assert_eq!(json, "\"AQIDBAUGBwgJCgsMDQ4PEA\"");
171        assert_eq!(id, serde_json::from_str(&json).unwrap());
172    }
173
174    #[test]
175    fn test_entity_id_bincode_serialization() {
176        let id = EntityId::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
177        let bytes = bincode::serialize(&id).unwrap();
178        assert_eq!(bytes, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
179        assert_eq!(id, bincode::deserialize(&bytes).unwrap());
180    }
181}
182
183// EntityId support for predicates
184
185impl From<EntityId> for ankql::ast::Expr {
186    fn from(id: EntityId) -> ankql::ast::Expr { ankql::ast::Expr::Literal(ankql::ast::Literal::EntityId(id.to_ulid())) }
187}
188
189impl From<&EntityId> for ankql::ast::Expr {
190    fn from(id: &EntityId) -> ankql::ast::Expr { ankql::ast::Expr::Literal(ankql::ast::Literal::EntityId(id.to_ulid())) }
191}