Skip to main content

context69_contracts/
library.rs

1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5use utoipa::{IntoParams, ToSchema};
6use uuid::Uuid;
7
8use super::Visibility;
9
10#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
11#[serde(rename_all = "snake_case")]
12pub enum LibraryIngestStatus {
13    Pending,
14    Running,
15    Succeeded,
16    Failed,
17}
18
19impl LibraryIngestStatus {
20    pub fn as_str(self) -> &'static str {
21        match self {
22            Self::Pending => "pending",
23            Self::Running => "running",
24            Self::Succeeded => "succeeded",
25            Self::Failed => "failed",
26        }
27    }
28}
29
30impl std::str::FromStr for LibraryIngestStatus {
31    type Err = anyhow::Error;
32
33    fn from_str(value: &str) -> Result<Self, Self::Err> {
34        match value {
35            "pending" => Ok(Self::Pending),
36            "running" => Ok(Self::Running),
37            "succeeded" => Ok(Self::Succeeded),
38            "failed" => Ok(Self::Failed),
39            other => Err(anyhow::anyhow!(
40                "unsupported library ingest status: {other}"
41            )),
42        }
43    }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
47pub struct CreateFolderRequest {
48    #[serde(default)]
49    pub parent_folder_id: Option<Uuid>,
50    pub name: String,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
54pub struct MoveFolderRequest {
55    #[serde(default)]
56    pub target_folder_id: Option<Uuid>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
60pub struct MoveFileRequest {
61    #[serde(default)]
62    pub target_folder_id: Option<Uuid>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
66pub struct CreateTextRequest {
67    #[serde(default)]
68    pub folder_id: Option<Uuid>,
69    pub title: String,
70    pub content: String,
71    #[serde(default = "default_text_content_format")]
72    pub content_format: LibraryTextContentFormat,
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub source_uri: Option<String>,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub summary: Option<String>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
80pub struct UpsertLibraryTextRequest {
81    pub external_id: String,
82    #[serde(default)]
83    pub folder_id: Option<Uuid>,
84    pub title: String,
85    pub content: String,
86    #[serde(default = "default_text_content_format")]
87    pub content_format: LibraryTextContentFormat,
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub source_uri: Option<String>,
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub summary: Option<String>,
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub published_at: Option<DateTime<Utc>>,
94    #[serde(default = "default_metadata_json")]
95    #[schema(value_type = Object)]
96    pub metadata_json: Value,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
100pub struct LibraryFileSummary {
101    pub file_id: Uuid,
102    pub group_key: String,
103    pub group_path: String,
104    pub visibility: Visibility,
105    #[serde(default)]
106    pub folder_id: Option<Uuid>,
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub external_id: Option<String>,
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub source_uri: Option<String>,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub published_at: Option<DateTime<Utc>>,
113    #[serde(default = "default_metadata_json")]
114    #[schema(value_type = Object)]
115    pub metadata_json: Value,
116    pub filename: String,
117    pub media_type: String,
118    pub size_bytes: i64,
119    pub ingest_status: LibraryIngestStatus,
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub error_message: Option<String>,
122    pub created_at: DateTime<Utc>,
123    pub updated_at: DateTime<Utc>,
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub ingested_at: Option<DateTime<Utc>>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
129pub struct LibraryFolderNode {
130    pub group_key: String,
131    pub group_path: String,
132    pub visibility: Visibility,
133    #[serde(default)]
134    pub folder_id: Option<Uuid>,
135    #[serde(default)]
136    pub parent_folder_id: Option<Uuid>,
137    pub name: String,
138    pub path: String,
139    pub processing_count: usize,
140    #[schema(no_recursion)]
141    pub children: Vec<LibraryFolderNode>,
142    pub files: Vec<LibraryFileSummary>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
146pub struct LibraryFolderResponse {
147    pub folder_id: Uuid,
148    pub group_key: String,
149    pub group_path: String,
150    pub visibility: Visibility,
151    #[serde(default)]
152    pub parent_folder_id: Option<Uuid>,
153    pub name: String,
154    pub path: String,
155    pub created_at: DateTime<Utc>,
156    pub updated_at: DateTime<Utc>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
160pub struct LibraryTreeResponse {
161    pub root: LibraryFolderNode,
162}
163
164#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
165#[serde(rename_all = "snake_case")]
166pub enum LibraryResourceKind {
167    Folder,
168    File,
169}
170
171#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
172#[serde(rename_all = "snake_case")]
173pub enum LibraryResourceSortBy {
174    Name,
175    Type,
176    Status,
177    Size,
178    UpdatedAt,
179}
180
181impl LibraryResourceSortBy {
182    pub fn as_str(self) -> &'static str {
183        match self {
184            Self::Name => "name",
185            Self::Type => "type",
186            Self::Status => "status",
187            Self::Size => "size",
188            Self::UpdatedAt => "updated_at",
189        }
190    }
191}
192
193#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
194#[serde(rename_all = "snake_case")]
195pub enum SortDirection {
196    Asc,
197    Desc,
198}
199
200impl SortDirection {
201    pub fn as_str(self) -> &'static str {
202        match self {
203            Self::Asc => "asc",
204            Self::Desc => "desc",
205        }
206    }
207}
208
209fn default_page() -> u32 {
210    1
211}
212
213fn default_page_size() -> u32 {
214    50
215}
216
217fn default_resource_sort_by() -> LibraryResourceSortBy {
218    LibraryResourceSortBy::UpdatedAt
219}
220
221fn default_sort_direction() -> SortDirection {
222    SortDirection::Desc
223}
224
225#[derive(Debug, Clone, Deserialize, IntoParams)]
226#[into_params(parameter_in = Query)]
227pub struct LibraryResourcePageQuery {
228    #[serde(default)]
229    pub folder_id: Option<Uuid>,
230    #[serde(default = "default_page")]
231    pub page: u32,
232    #[serde(default = "default_page_size")]
233    pub page_size: u32,
234    #[serde(default)]
235    pub query: Option<String>,
236    #[serde(default)]
237    pub status: Option<LibraryIngestStatus>,
238    #[serde(default = "default_resource_sort_by")]
239    pub sort_by: LibraryResourceSortBy,
240    #[serde(default = "default_sort_direction")]
241    pub sort_direction: SortDirection,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
245pub struct LibraryResourceItem {
246    pub kind: LibraryResourceKind,
247    pub id: Uuid,
248    pub group_key: String,
249    pub group_path: String,
250    pub visibility: Visibility,
251    #[serde(default)]
252    pub parent_folder_id: Option<Uuid>,
253    pub name: String,
254    #[serde(default, skip_serializing_if = "Option::is_none")]
255    pub media_type: Option<String>,
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub size_bytes: Option<i64>,
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub ingest_status: Option<LibraryIngestStatus>,
260    #[serde(default, skip_serializing_if = "Option::is_none")]
261    pub error_message: Option<String>,
262    pub child_folder_count: u64,
263    pub file_count: u64,
264    pub processing_count: u64,
265    pub is_source_folder: bool,
266    pub is_source_records_folder: bool,
267    pub created_at: DateTime<Utc>,
268    pub updated_at: DateTime<Utc>,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
272pub struct LibraryResourcePageResponse {
273    pub items: Vec<LibraryResourceItem>,
274    pub page: u32,
275    pub page_size: u32,
276    pub total: u64,
277    pub total_pages: u32,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
281pub struct LibraryIngestJobResponse {
282    pub job_id: Uuid,
283    pub group_key: String,
284    pub group_path: String,
285    pub visibility: Visibility,
286    pub file_id: Uuid,
287    pub status: LibraryIngestStatus,
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub docling_task_id: Option<String>,
290    #[serde(default, skip_serializing_if = "Option::is_none")]
291    pub error_message: Option<String>,
292    pub created_at: DateTime<Utc>,
293    #[serde(default, skip_serializing_if = "Option::is_none")]
294    pub started_at: Option<DateTime<Utc>>,
295    #[serde(default, skip_serializing_if = "Option::is_none")]
296    pub finished_at: Option<DateTime<Utc>>,
297    pub updated_at: DateTime<Utc>,
298}
299
300#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
301#[serde(rename_all = "snake_case")]
302pub enum LibraryTextContentFormat {
303    PlainText,
304    Markdown,
305}
306
307#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
308#[serde(rename_all = "snake_case")]
309pub enum LibraryPreviewContentFormat {
310    PlainText,
311    Markdown,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
315pub struct LibraryDocumentSectionPreview {
316    pub document_id: i64,
317    pub section_key: String,
318    pub section_label: String,
319    pub sort_order: i32,
320    pub title: String,
321    pub preview_text: String,
322    #[serde(default = "default_preview_content_format")]
323    pub content_format: LibraryPreviewContentFormat,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
327pub struct LibraryFileDetailResponse {
328    pub file_id: Uuid,
329    pub group_key: String,
330    pub group_path: String,
331    pub visibility: Visibility,
332    #[serde(default)]
333    pub folder_id: Option<Uuid>,
334    pub folder_path: String,
335    pub filename: String,
336    pub media_type: String,
337    pub size_bytes: i64,
338    pub sha256: String,
339    #[serde(default)]
340    pub source_available: bool,
341    pub ingest_status: LibraryIngestStatus,
342    #[serde(default, skip_serializing_if = "Option::is_none")]
343    pub error_message: Option<String>,
344    pub created_at: DateTime<Utc>,
345    pub updated_at: DateTime<Utc>,
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    pub ingested_at: Option<DateTime<Utc>>,
348    pub sections: Vec<LibraryDocumentSectionPreview>,
349    pub jobs: Vec<LibraryIngestJobResponse>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
353pub struct LibraryUploadResponse {
354    pub files: Vec<LibraryFileSummary>,
355    pub jobs: Vec<LibraryIngestJobResponse>,
356}
357
358#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
359pub struct LibraryFileUploadMetadata {
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    pub external_id: Option<String>,
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub source_uri: Option<String>,
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub published_at: Option<DateTime<Utc>>,
366    #[serde(default = "default_metadata_json")]
367    #[schema(value_type = Object)]
368    pub metadata_json: Value,
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
372pub struct PrepareLibraryUploadRequest {
373    #[serde(default)]
374    pub folder_id: Option<Uuid>,
375    pub filename: String,
376    pub media_type: String,
377    pub size_bytes: i64,
378    pub sha256: String,
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub metadata: Option<LibraryFileUploadMetadata>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
384pub struct PrepareLibraryUploadResponse {
385    pub upload_required: bool,
386    #[serde(default, skip_serializing_if = "Option::is_none")]
387    pub file: Option<LibraryFileSummary>,
388    #[serde(default, skip_serializing_if = "Option::is_none")]
389    pub job: Option<LibraryIngestJobResponse>,
390}
391
392fn default_preview_content_format() -> LibraryPreviewContentFormat {
393    LibraryPreviewContentFormat::PlainText
394}
395
396fn default_text_content_format() -> LibraryTextContentFormat {
397    LibraryTextContentFormat::PlainText
398}
399
400fn default_metadata_json() -> Value {
401    json!({})
402}