Skip to main content

systemprompt_files/models/
metadata.rs

1//! Structured metadata stored alongside file rows.
2//!
3//! [`FileMetadata`] is the top-level container, carrying optional
4//! [`FileChecksums`] and a [`TypeSpecificMetadata`] variant
5//! ([`DocumentMetadata`], [`AudioMetadata`], or [`VideoMetadata`]; image detail
6//! lives in the sibling `image_metadata` module). Each type offers a builder
7//! API for incremental construction. Unrecognised keys on stored rows are
8//! preserved verbatim in [`FileMetadata::extra`], so arbitrary historical
9//! shapes decode losslessly.
10//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use serde::{Deserialize, Serialize};
15
16pub(super) use super::image_metadata::ImageMetadata;
17
18#[derive(Debug, Clone, Default, Serialize, Deserialize)]
19pub struct FileMetadata {
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub checksums: Option<FileChecksums>,
22
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub type_specific: Option<TypeSpecificMetadata>,
25
26    #[serde(flatten)]
27    pub extra: serde_json::Map<String, serde_json::Value>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(tag = "type", rename_all = "snake_case")]
32pub enum TypeSpecificMetadata {
33    Image(ImageMetadata),
34    Document(DocumentMetadata),
35    Audio(AudioMetadata),
36    Video(VideoMetadata),
37}
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
40pub struct DocumentMetadata {
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub title: Option<String>,
43
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub author: Option<String>,
46
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub page_count: Option<u32>,
49}
50
51#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
52pub struct AudioMetadata {
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub duration_seconds: Option<f32>,
55
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub sample_rate: Option<u32>,
58
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub channels: Option<u8>,
61}
62
63#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
64pub struct VideoMetadata {
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub width: Option<u32>,
67
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub height: Option<u32>,
70
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub duration_seconds: Option<f32>,
73
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub frame_rate: Option<f32>,
76}
77
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct FileChecksums {
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub md5: Option<String>,
82
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub sha256: Option<String>,
85}
86
87impl FileMetadata {
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    pub fn with_image(mut self, image: ImageMetadata) -> Self {
93        self.type_specific = Some(TypeSpecificMetadata::Image(image));
94        self
95    }
96
97    pub fn with_document(mut self, doc: DocumentMetadata) -> Self {
98        self.type_specific = Some(TypeSpecificMetadata::Document(doc));
99        self
100    }
101
102    pub fn with_audio(mut self, audio: AudioMetadata) -> Self {
103        self.type_specific = Some(TypeSpecificMetadata::Audio(audio));
104        self
105    }
106
107    pub fn with_video(mut self, video: VideoMetadata) -> Self {
108        self.type_specific = Some(TypeSpecificMetadata::Video(video));
109        self
110    }
111
112    pub fn with_checksums(mut self, checksums: FileChecksums) -> Self {
113        self.checksums = Some(checksums);
114        self
115    }
116}
117
118impl DocumentMetadata {
119    pub const fn new() -> Self {
120        Self {
121            title: None,
122            author: None,
123            page_count: None,
124        }
125    }
126
127    pub fn with_title(mut self, title: impl Into<String>) -> Self {
128        self.title = Some(title.into());
129        self
130    }
131
132    pub fn with_author(mut self, author: impl Into<String>) -> Self {
133        self.author = Some(author.into());
134        self
135    }
136
137    pub const fn with_page_count(mut self, page_count: u32) -> Self {
138        self.page_count = Some(page_count);
139        self
140    }
141}
142
143impl AudioMetadata {
144    pub const fn new() -> Self {
145        Self {
146            duration_seconds: None,
147            sample_rate: None,
148            channels: None,
149        }
150    }
151
152    pub const fn with_duration_seconds(mut self, duration: f32) -> Self {
153        self.duration_seconds = Some(duration);
154        self
155    }
156
157    pub const fn with_sample_rate(mut self, sample_rate: u32) -> Self {
158        self.sample_rate = Some(sample_rate);
159        self
160    }
161
162    pub const fn with_channels(mut self, channels: u8) -> Self {
163        self.channels = Some(channels);
164        self
165    }
166}
167
168impl VideoMetadata {
169    pub const fn new() -> Self {
170        Self {
171            width: None,
172            height: None,
173            duration_seconds: None,
174            frame_rate: None,
175        }
176    }
177
178    pub const fn with_dimensions(mut self, width: u32, height: u32) -> Self {
179        self.width = Some(width);
180        self.height = Some(height);
181        self
182    }
183
184    pub const fn with_duration_seconds(mut self, duration: f32) -> Self {
185        self.duration_seconds = Some(duration);
186        self
187    }
188
189    pub const fn with_frame_rate(mut self, frame_rate: f32) -> Self {
190        self.frame_rate = Some(frame_rate);
191        self
192    }
193}
194
195impl FileChecksums {
196    pub const fn new() -> Self {
197        Self {
198            md5: None,
199            sha256: None,
200        }
201    }
202
203    pub fn with_md5(mut self, md5: impl Into<String>) -> Self {
204        self.md5 = Some(md5.into());
205        self
206    }
207
208    pub fn with_sha256(mut self, sha256: impl Into<String>) -> Self {
209        self.sha256 = Some(sha256.into());
210        self
211    }
212}