1use std::ops::Deref;
8
9use kalamdb_commons::TableId;
10use serde::{Deserialize, Serialize};
11
12pub use kalamdb_commons::FileRef;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(transparent)]
17pub struct FileRefContext(TableId);
18
19impl FileRefContext {
20 pub fn new(table_id: TableId) -> Self {
22 Self(table_id)
23 }
24
25 pub fn from_strings(namespace: &str, table: &str) -> Self {
27 Self(TableId::from_strings(namespace, table))
28 }
29
30 pub fn table_id(&self) -> &TableId {
32 &self.0
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct BoundFileRef {
39 file_ref: FileRef,
40 context: FileRefContext,
41}
42
43impl BoundFileRef {
44 pub fn new(file_ref: FileRef, context: FileRefContext) -> Self {
46 Self { file_ref, context }
47 }
48
49 pub fn file_ref(&self) -> &FileRef {
51 &self.file_ref
52 }
53
54 pub fn into_file_ref(self) -> FileRef {
56 self.file_ref
57 }
58
59 pub fn context(&self) -> &FileRefContext {
61 &self.context
62 }
63
64 pub fn table_id(&self) -> &TableId {
66 self.context.table_id()
67 }
68
69 pub fn namespace(&self) -> &str {
71 self.context.table_id().namespace_id().as_str()
72 }
73
74 pub fn table(&self) -> &str {
76 self.context.table_id().table_name().as_str()
77 }
78
79 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 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}