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