Skip to main content

ankurah_core/property/value/
entity_ref.rs

1//! Typed entity reference property.
2//!
3//! The `Ref<T>` type wraps an `EntityId` with compile-time knowledge of the target model type.
4//! This enables type-safe entity traversal:
5//!
6//! ```rust,ignore
7//! #[derive(Model)]
8//! pub struct Album {
9//!     pub name: String,
10//!     pub artist: Ref<Artist>,
11//! }
12//!
13//! // Fetch referenced entity
14//! let album: AlbumView = ctx.get(album_id).await?;
15//! let artist: ArtistView = album.artist().get(&ctx).await?;
16//! ```
17
18use crate::model::View;
19use ankurah_proto::EntityId;
20use serde::{Deserialize, Serialize};
21use std::borrow::Borrow;
22use std::fmt;
23use std::marker::PhantomData;
24use std::ops::Deref;
25
26use crate::context::Context;
27use crate::error::RetrievalError;
28use crate::model::Model;
29use crate::property::{Property, PropertyError};
30use crate::value::Value;
31
32/// A typed reference to another entity.
33///
34/// Stores an `EntityId` internally but carries compile-time type information
35/// about the target model, enabling type-safe `.get()` calls.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(transparent)]
38pub struct Ref<T> {
39    id: EntityId,
40    #[serde(skip)]
41    _phantom: PhantomData<T>,
42}
43
44impl<T> Deref for Ref<T> {
45    type Target = EntityId;
46    fn deref(&self) -> &EntityId { &self.id }
47}
48
49impl<T> AsRef<EntityId> for Ref<T> {
50    fn as_ref(&self) -> &EntityId { &self.id }
51}
52
53impl<T> Borrow<EntityId> for Ref<T> {
54    fn borrow(&self) -> &EntityId { &self.id }
55}
56
57impl<T> Ref<T> {
58    /// Create a new Ref from an EntityId.
59    pub fn new(id: EntityId) -> Self { Ref { id, _phantom: PhantomData } }
60
61    /// Create a Ref from a base64-encoded EntityId string.
62    pub fn from_base64(s: &str) -> Result<Self, ankurah_proto::DecodeError> { Ok(Ref::new(EntityId::from_base64(s)?)) }
63
64    /// Get the underlying EntityId.
65    pub fn id(&self) -> EntityId { self.id.clone() }
66
67    /// Get the underlying EntityId as a reference.
68    pub fn id_ref(&self) -> &EntityId { &self.id }
69}
70
71impl<T: Model> Ref<T> {
72    /// Fetch the referenced entity from the given context.
73    ///
74    /// # Example
75    /// ```rust,ignore
76    /// let album: AlbumView = ctx.get(album_id).await?;
77    /// let artist: ArtistView = album.artist().get(&ctx).await?;
78    /// ```
79    pub async fn get(&self, ctx: &Context) -> Result<T::View, RetrievalError> { ctx.get::<T::View>(self.id.clone()).await }
80}
81
82impl<T> From<EntityId> for Ref<T> {
83    fn from(id: EntityId) -> Self { Ref::new(id) }
84}
85
86impl<T> From<&EntityId> for Ref<T> {
87    fn from(id: &EntityId) -> Self { Ref::new(id.clone()) }
88}
89
90impl<T> TryFrom<&str> for Ref<T> {
91    type Error = ankurah_proto::DecodeError;
92    fn try_from(s: &str) -> Result<Self, Self::Error> { Ref::from_base64(s) }
93}
94
95impl<T> TryFrom<String> for Ref<T> {
96    type Error = ankurah_proto::DecodeError;
97    fn try_from(s: String) -> Result<Self, Self::Error> { Ref::from_base64(&s) }
98}
99
100impl<T> From<Ref<T>> for EntityId {
101    fn from(r: Ref<T>) -> Self { r.id }
102}
103
104impl<T> From<&Ref<T>> for EntityId {
105    fn from(r: &Ref<T>) -> Self { r.id.clone() }
106}
107
108impl<T> fmt::Display for Ref<T> {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.id.to_base64()) }
110}
111
112// Ref<T> support for predicates (queries)
113impl<T> From<Ref<T>> for ::ankql::ast::Expr {
114    fn from(r: Ref<T>) -> ::ankql::ast::Expr { r.id.into() }
115}
116
117impl<T> From<&Ref<T>> for ::ankql::ast::Expr {
118    fn from(r: &Ref<T>) -> ::ankql::ast::Expr { (&r.id).into() }
119}
120
121// Any View can be converted to Ref<Model> by borrowing
122impl<V: View> From<&V> for Ref<V::Model> {
123    fn from(view: &V) -> Ref<V::Model> { Ref::new(view.id()) }
124}
125
126impl<T> Property for Ref<T> {
127    fn into_value(&self) -> Result<Option<Value>, PropertyError> { Ok(Some(Value::EntityId(self.id.clone()))) }
128
129    fn from_value(value: Option<Value>) -> Result<Self, PropertyError> {
130        match value {
131            Some(Value::EntityId(id)) => Ok(Ref::new(id)),
132            // Backwards compatibility: accept string EntityIds (e.g., from older schema)
133            Some(Value::String(s)) => {
134                EntityId::from_base64(&s).map(Ref::new).map_err(|e| PropertyError::InvalidValue { value: s, ty: format!("Ref ({})", e) })
135            }
136            Some(other) => Err(PropertyError::InvalidVariant { given: other, ty: "Ref".to_string() }),
137            None => Err(PropertyError::Missing),
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    // Dummy model for testing
147    struct TestModel;
148
149    #[test]
150    fn test_ref_roundtrip() {
151        let id = EntityId::new();
152        let r: Ref<TestModel> = Ref::new(id.clone());
153
154        let value = r.into_value().unwrap().unwrap();
155        assert!(matches!(value, Value::EntityId(_)));
156
157        let recovered: Ref<TestModel> = Ref::from_value(Some(value)).unwrap();
158        assert_eq!(recovered.id(), id);
159    }
160
161    #[test]
162    fn test_ref_from_entity_id() {
163        let id = EntityId::new();
164        let r: Ref<TestModel> = id.clone().into();
165        assert_eq!(r.id(), id);
166    }
167
168    #[test]
169    fn test_ref_into_entity_id() {
170        let id = EntityId::new();
171        let r: Ref<TestModel> = Ref::new(id.clone());
172        let recovered: EntityId = r.into();
173        assert_eq!(recovered, id);
174    }
175
176    #[test]
177    fn test_ref_missing() {
178        let result: Result<Ref<TestModel>, _> = Ref::from_value(None);
179        assert!(matches!(result, Err(PropertyError::Missing)));
180    }
181
182    #[test]
183    fn test_ref_invalid_string() {
184        // Invalid base64 string should return InvalidValue (backwards compat path tries to parse)
185        let result: Result<Ref<TestModel>, _> = Ref::from_value(Some(Value::String("not an id".to_string())));
186        assert!(matches!(result, Err(PropertyError::InvalidValue { .. })));
187    }
188
189    #[test]
190    fn test_ref_invalid_variant() {
191        // Completely wrong type should return InvalidVariant
192        let result: Result<Ref<TestModel>, _> = Ref::from_value(Some(Value::I64(42)));
193        assert!(matches!(result, Err(PropertyError::InvalidVariant { .. })));
194    }
195}