Skip to main content

link_common/models/
file_ref.rs

1//! FILE column reference helpers for KalamDB SDKs.
2//!
3//! The core [`FileRef`] JSON model lives in `kalamdb-commons`. This module adds
4//! client-side context binding so SDK callers can generate URLs or download a
5//! file without passing namespace/table repeatedly.
6
7use std::ops::Deref;
8
9use kalamdb_commons::TableId;
10use serde::{Deserialize, Serialize};
11
12pub use kalamdb_commons::FileRef;
13
14/// Table context needed to locate a FILE column value for downloads.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(transparent)]
17pub struct FileRefContext(TableId);
18
19impl FileRefContext {
20    /// Create a new file reference context from a [`TableId`].
21    pub fn new(table_id: TableId) -> Self {
22        Self(table_id)
23    }
24
25    /// Create a file reference context from namespace and table name strings.
26    pub fn from_strings(namespace: &str, table: &str) -> Self {
27        Self(TableId::from_strings(namespace, table))
28    }
29
30    /// Borrow the table identity attached to this context.
31    pub fn table_id(&self) -> &TableId {
32        &self.0
33    }
34}
35
36/// A [`FileRef`] with table context attached.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct BoundFileRef {
39    file_ref: FileRef,
40    context: FileRefContext,
41}
42
43impl BoundFileRef {
44    /// Create a bound file reference from a raw file reference and context.
45    pub fn new(file_ref: FileRef, context: FileRefContext) -> Self {
46        Self { file_ref, context }
47    }
48
49    /// Borrow the underlying storage/wire file reference.
50    pub fn file_ref(&self) -> &FileRef {
51        &self.file_ref
52    }
53
54    /// Consume the bound reference and return the underlying file reference.
55    pub fn into_file_ref(self) -> FileRef {
56        self.file_ref
57    }
58
59    /// Borrow the context attached to this file reference.
60    pub fn context(&self) -> &FileRefContext {
61        &self.context
62    }
63
64    /// Table that owns the FILE column.
65    pub fn table_id(&self) -> &TableId {
66        self.context.table_id()
67    }
68
69    /// Namespace that owns the table.
70    pub fn namespace(&self) -> &str {
71        self.context.table_id().namespace_id().as_str()
72    }
73
74    /// Table that owns the FILE column.
75    pub fn table(&self) -> &str {
76        self.context.table_id().table_name().as_str()
77    }
78
79    /// Full download URL for this file.
80    pub fn download_url(&self, base_url: &str) -> String {
81        self.file_ref
82            .download_url(base_url, self.namespace(), self.table())
83    }
84
85    /// Relative HTTP path for this file.
86    pub fn relative_url(&self) -> String {
87        self.file_ref.relative_url(self.namespace(), self.table())
88    }
89}
90
91impl Deref for BoundFileRef {
92    type Target = FileRef;
93
94    fn deref(&self) -> &Self::Target {
95        &self.file_ref
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn parse_from_json_string() {
105        let json = r#"{"id":"123","sub":"f0001","name":"test.png","size":1024,"mime":"image/png","sha256":"abc"}"#;
106        let fr = FileRef::from_json(json).unwrap();
107        assert_eq!(fr.id, "123");
108        assert_eq!(fr.sub, "f0001");
109        assert_eq!(fr.name, "test.png");
110        assert_eq!(fr.size, 1024);
111        assert!(fr.is_image());
112    }
113
114    #[test]
115    fn parse_from_json_value_object() {
116        let val = serde_json::json!({
117            "id": "456", "sub": "f0002", "name": "doc.pdf",
118            "size": 2048, "mime": "application/pdf", "sha256": "def"
119        });
120        let fr = FileRef::from_json_value(&val).unwrap();
121        assert!(fr.is_pdf());
122        assert_eq!(fr.type_description(), "PDF Document");
123    }
124
125    #[test]
126    fn parse_from_json_value_string() {
127        let inner = r#"{"id":"789","sub":"f0001","name":"a.txt","size":10,"mime":"text/plain","sha256":"x"}"#;
128        let val = serde_json::Value::String(inner.to_string());
129        let fr = FileRef::from_json_value(&val).unwrap();
130        assert_eq!(fr.id, "789");
131    }
132
133    #[test]
134    fn download_url_generation() {
135        let fr = FileRef {
136            id: "123".into(),
137            sub: "f0001".into(),
138            name: "t.png".into(),
139            size: 0,
140            mime: "image/png".into(),
141            sha256: String::new(),
142            shard: None,
143        };
144        assert_eq!(
145            fr.download_url("http://localhost:2900", "default", "users"),
146            "http://localhost:2900/v1/files/default/users/f0001/123-t.png"
147        );
148        assert_eq!(fr.relative_url("default", "users"), "/v1/files/default/users/f0001/123-t.png");
149    }
150
151    #[test]
152    fn bound_file_ref_uses_table_context_for_urls() {
153        let fr = FileRef {
154            id: "123".into(),
155            sub: "f0001".into(),
156            name: "t.png".into(),
157            size: 0,
158            mime: "image/png".into(),
159            sha256: String::new(),
160            shard: None,
161        };
162        let ctx = FileRefContext::from_strings("default", "users");
163        let bound = BoundFileRef::new(fr, ctx);
164
165        assert_eq!(bound.namespace(), "default");
166        assert_eq!(bound.table(), "users");
167        assert_eq!(
168            bound.download_url("http://localhost:2900/"),
169            "http://localhost:2900/v1/files/default/users/f0001/123-t.png"
170        );
171        assert_eq!(bound.relative_url(), "/v1/files/default/users/f0001/123-t.png");
172        assert_eq!(bound.file_ref().name, "t.png");
173    }
174
175    #[test]
176    fn format_size_units() {
177        let mk = |size: u64| FileRef {
178            id: String::new(),
179            sub: String::new(),
180            name: String::new(),
181            size,
182            mime: String::new(),
183            sha256: String::new(),
184            shard: None,
185        };
186        assert_eq!(mk(0).format_size(), "0 B");
187        assert_eq!(mk(512).format_size(), "512 B");
188        assert_eq!(mk(1024).format_size(), "1.0 KB");
189        assert_eq!(mk(1_048_576).format_size(), "1.0 MB");
190    }
191
192    #[test]
193    fn stored_name_and_path() {
194        let fr = FileRef {
195            id: "42".into(),
196            sub: "f0001".into(),
197            name: "My Document.pdf".into(),
198            size: 100,
199            mime: "application/pdf".into(),
200            sha256: String::new(),
201            shard: None,
202        };
203        assert_eq!(fr.stored_name(), "42-my-document.pdf");
204        assert_eq!(fr.relative_path(), "f0001/42-my-document.pdf");
205    }
206
207    #[test]
208    fn stored_name_with_shard() {
209        let fr = FileRef {
210            id: "42".into(),
211            sub: "f0001".into(),
212            name: "test.png".into(),
213            size: 100,
214            mime: "image/png".into(),
215            sha256: String::new(),
216            shard: Some(3),
217        };
218        assert_eq!(fr.relative_path(), "shard-3/f0001/42-test.png");
219    }
220
221    #[test]
222    fn cell_as_file() {
223        use super::super::kalam_cell_value::KalamCellValue;
224
225        let cell = KalamCellValue::from(serde_json::json!({
226            "id": "1", "sub": "f0001", "name": "a.png",
227            "size": 10, "mime": "image/png", "sha256": "x"
228        }));
229        let fr = cell.as_file().unwrap();
230        assert_eq!(fr.id, "1");
231        assert!(fr.is_image());
232
233        assert!(KalamCellValue::text("Alice").as_file().is_none());
234        assert!(KalamCellValue::null().as_file().is_none());
235    }
236
237    #[test]
238    fn cell_as_bound_file_attaches_context() {
239        use super::super::kalam_cell_value::KalamCellValue;
240
241        let cell = KalamCellValue::from(serde_json::json!({
242            "id": "1", "sub": "f0001", "name": "a.png",
243            "size": 10, "mime": "image/png", "sha256": "x"
244        }));
245
246        let table_id = TableId::from_strings("docs", "files");
247        let bound = cell.as_bound_file(&table_id).expect("FILE column");
248        assert_eq!(bound.namespace(), "docs");
249        assert_eq!(bound.table(), "files");
250        assert_eq!(bound.table_id(), &table_id);
251        assert_eq!(bound.file_ref().id, "1");
252        assert_eq!(bound.relative_url(), "/v1/files/docs/files/f0001/1-a.png");
253    }
254}