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    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub translation: Option<crate::TranslationDirective>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
82pub struct UpsertLibraryTextRequest {
83    pub external_id: String,
84    #[serde(default)]
85    pub folder_id: Option<Uuid>,
86    pub title: String,
87    pub content: String,
88    #[serde(default = "default_text_content_format")]
89    pub content_format: LibraryTextContentFormat,
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub source_uri: Option<String>,
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub summary: Option<String>,
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub published_at: Option<DateTime<Utc>>,
96    #[serde(default = "default_metadata_json")]
97    #[schema(value_type = Object)]
98    pub metadata_json: Value,
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub translation: Option<crate::TranslationDirective>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
104pub struct LibraryFileSummary {
105    pub file_id: Uuid,
106    pub group_key: String,
107    pub group_path: String,
108    pub visibility: Visibility,
109    #[serde(default)]
110    pub folder_id: Option<Uuid>,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub external_id: Option<String>,
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub source_uri: Option<String>,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub published_at: Option<DateTime<Utc>>,
117    #[serde(default = "default_metadata_json")]
118    #[schema(value_type = Object)]
119    pub metadata_json: Value,
120    pub filename: String,
121    pub media_type: String,
122    pub size_bytes: i64,
123    pub ingest_status: LibraryIngestStatus,
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub error_message: Option<String>,
126    pub created_at: DateTime<Utc>,
127    pub updated_at: DateTime<Utc>,
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub ingested_at: Option<DateTime<Utc>>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
133pub struct LibraryFolderNode {
134    pub group_key: String,
135    pub group_path: String,
136    pub visibility: Visibility,
137    #[serde(default)]
138    pub folder_id: Option<Uuid>,
139    #[serde(default)]
140    pub parent_folder_id: Option<Uuid>,
141    pub name: String,
142    pub path: String,
143    pub processing_count: usize,
144    #[schema(no_recursion)]
145    pub children: Vec<LibraryFolderNode>,
146    pub files: Vec<LibraryFileSummary>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
150pub struct LibraryFolderResponse {
151    pub folder_id: Uuid,
152    pub group_key: String,
153    pub group_path: String,
154    pub visibility: Visibility,
155    #[serde(default)]
156    pub parent_folder_id: Option<Uuid>,
157    pub name: String,
158    pub path: String,
159    pub created_at: DateTime<Utc>,
160    pub updated_at: DateTime<Utc>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
164pub struct LibraryTreeResponse {
165    pub root: LibraryFolderNode,
166}
167
168#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
169#[serde(rename_all = "snake_case")]
170pub enum LibraryResourceKind {
171    Folder,
172    File,
173}
174
175#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
176#[serde(rename_all = "snake_case")]
177pub enum LibraryResourceSortBy {
178    Name,
179    Type,
180    Status,
181    Size,
182    UpdatedAt,
183}
184
185impl LibraryResourceSortBy {
186    pub fn as_str(self) -> &'static str {
187        match self {
188            Self::Name => "name",
189            Self::Type => "type",
190            Self::Status => "status",
191            Self::Size => "size",
192            Self::UpdatedAt => "updated_at",
193        }
194    }
195}
196
197#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
198#[serde(rename_all = "snake_case")]
199pub enum SortDirection {
200    Asc,
201    Desc,
202}
203
204impl SortDirection {
205    pub fn as_str(self) -> &'static str {
206        match self {
207            Self::Asc => "asc",
208            Self::Desc => "desc",
209        }
210    }
211}
212
213fn default_page() -> u32 {
214    1
215}
216
217fn default_page_size() -> u32 {
218    50
219}
220
221fn default_resource_sort_by() -> LibraryResourceSortBy {
222    LibraryResourceSortBy::UpdatedAt
223}
224
225fn default_sort_direction() -> SortDirection {
226    SortDirection::Desc
227}
228
229#[derive(Debug, Clone, Deserialize, IntoParams)]
230#[into_params(parameter_in = Query)]
231pub struct LibraryResourcePageQuery {
232    #[serde(default)]
233    pub folder_id: Option<Uuid>,
234    #[serde(default = "default_page")]
235    pub page: u32,
236    #[serde(default = "default_page_size")]
237    pub page_size: u32,
238    #[serde(default)]
239    pub query: Option<String>,
240    #[serde(default)]
241    pub status: Option<LibraryIngestStatus>,
242    #[serde(default = "default_resource_sort_by")]
243    pub sort_by: LibraryResourceSortBy,
244    #[serde(default = "default_sort_direction")]
245    pub sort_direction: SortDirection,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
249pub struct LibraryResourceItem {
250    pub kind: LibraryResourceKind,
251    pub id: Uuid,
252    pub group_key: String,
253    pub group_path: String,
254    pub visibility: Visibility,
255    #[serde(default)]
256    pub parent_folder_id: Option<Uuid>,
257    pub name: String,
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub media_type: Option<String>,
260    #[serde(default, skip_serializing_if = "Option::is_none")]
261    pub size_bytes: Option<i64>,
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub ingest_status: Option<LibraryIngestStatus>,
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub error_message: Option<String>,
266    pub child_folder_count: u64,
267    pub file_count: u64,
268    pub processing_count: u64,
269    pub is_source_folder: bool,
270    pub is_source_records_folder: bool,
271    pub created_at: DateTime<Utc>,
272    pub updated_at: DateTime<Utc>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
276pub struct LibraryResourcePageResponse {
277    pub items: Vec<LibraryResourceItem>,
278    pub page: u32,
279    pub page_size: u32,
280    pub total: u64,
281    pub total_pages: u32,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
285pub struct LibraryIngestJobResponse {
286    pub job_id: Uuid,
287    pub group_key: String,
288    pub group_path: String,
289    pub visibility: Visibility,
290    pub file_id: Uuid,
291    pub status: LibraryIngestStatus,
292    #[serde(default, skip_serializing_if = "Option::is_none")]
293    pub docling_task_id: Option<String>,
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub error_message: Option<String>,
296    pub created_at: DateTime<Utc>,
297    #[serde(default, skip_serializing_if = "Option::is_none")]
298    pub started_at: Option<DateTime<Utc>>,
299    #[serde(default, skip_serializing_if = "Option::is_none")]
300    pub finished_at: Option<DateTime<Utc>>,
301    pub updated_at: DateTime<Utc>,
302}
303
304#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
305#[serde(rename_all = "snake_case")]
306pub enum LibraryTextContentFormat {
307    PlainText,
308    Markdown,
309}
310
311#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
312#[serde(rename_all = "snake_case")]
313pub enum LibraryPreviewContentFormat {
314    PlainText,
315    Markdown,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
319pub struct LibraryDocumentSectionPreview {
320    pub document_id: i64,
321    pub section_key: String,
322    pub section_label: String,
323    pub sort_order: i32,
324    pub title: String,
325    pub preview_text: String,
326    #[serde(default = "default_preview_content_format")]
327    pub content_format: LibraryPreviewContentFormat,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
331pub struct LibraryFileDetailResponse {
332    pub file_id: Uuid,
333    pub group_key: String,
334    pub group_path: String,
335    pub visibility: Visibility,
336    #[serde(default)]
337    pub folder_id: Option<Uuid>,
338    pub folder_path: String,
339    pub filename: String,
340    pub media_type: String,
341    pub size_bytes: i64,
342    pub sha256: String,
343    #[serde(default)]
344    pub source_available: bool,
345    pub ingest_status: LibraryIngestStatus,
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    pub error_message: Option<String>,
348    pub created_at: DateTime<Utc>,
349    pub updated_at: DateTime<Utc>,
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub ingested_at: Option<DateTime<Utc>>,
352    pub sections: Vec<LibraryDocumentSectionPreview>,
353    pub jobs: Vec<LibraryIngestJobResponse>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
357pub struct LibraryUploadResponse {
358    pub files: Vec<LibraryFileSummary>,
359    pub jobs: Vec<LibraryIngestJobResponse>,
360}
361
362#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
363pub struct LibraryFileUploadMetadata {
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub external_id: Option<String>,
366    #[serde(default, skip_serializing_if = "Option::is_none")]
367    pub source_uri: Option<String>,
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub published_at: Option<DateTime<Utc>>,
370    #[serde(default = "default_metadata_json")]
371    #[schema(value_type = Object)]
372    pub metadata_json: Value,
373}
374
375#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
376pub struct LibraryFileIngestOptions {
377    #[serde(flatten)]
378    pub metadata: LibraryFileUploadMetadata,
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub translation: Option<crate::TranslationDirective>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
384pub struct PrepareLibraryUploadRequest {
385    #[serde(default)]
386    pub folder_id: Option<Uuid>,
387    pub filename: String,
388    pub media_type: String,
389    pub size_bytes: i64,
390    pub sha256: String,
391    #[serde(default, skip_serializing_if = "Option::is_none")]
392    pub metadata: Option<LibraryFileUploadMetadata>,
393    #[serde(default, skip_serializing_if = "Option::is_none")]
394    pub translation: Option<crate::TranslationDirective>,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
398pub struct PrepareLibraryUploadResponse {
399    pub upload_required: bool,
400    #[serde(default, skip_serializing_if = "Option::is_none")]
401    pub file: Option<LibraryFileSummary>,
402    #[serde(default, skip_serializing_if = "Option::is_none")]
403    pub job: Option<LibraryIngestJobResponse>,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
407pub struct ImportLibraryFileFromUrlRequest {
408    pub url: String,
409    #[serde(default, skip_serializing_if = "Option::is_none")]
410    pub folder_id: Option<Uuid>,
411    #[serde(default, skip_serializing_if = "Option::is_none")]
412    pub filename: Option<String>,
413    #[serde(default, skip_serializing_if = "Option::is_none")]
414    pub media_type: Option<String>,
415    #[serde(default, skip_serializing_if = "Option::is_none")]
416    pub metadata: Option<LibraryFileUploadMetadata>,
417    #[serde(default, skip_serializing_if = "Option::is_none")]
418    pub translation: Option<crate::TranslationDirective>,
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
422#[serde(rename_all = "snake_case")]
423pub enum LibraryUrlImportStatus {
424    Queued,
425    Downloading,
426    Ingesting,
427    Succeeded,
428    Failed,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
432pub struct LibraryUrlImportJobResponse {
433    pub import_job_id: Uuid,
434    pub group_path: String,
435    pub source_url: String,
436    pub status: LibraryUrlImportStatus,
437    pub attempt_count: i32,
438    #[serde(default, skip_serializing_if = "Option::is_none")]
439    pub file: Option<LibraryFileSummary>,
440    #[serde(default, skip_serializing_if = "Option::is_none")]
441    pub ingest_job: Option<LibraryIngestJobResponse>,
442    #[serde(default, skip_serializing_if = "Option::is_none")]
443    pub error_code: Option<String>,
444    #[serde(default, skip_serializing_if = "Option::is_none")]
445    pub error_message: Option<String>,
446    pub created_at: DateTime<Utc>,
447    #[serde(default, skip_serializing_if = "Option::is_none")]
448    pub started_at: Option<DateTime<Utc>>,
449    #[serde(default, skip_serializing_if = "Option::is_none")]
450    pub finished_at: Option<DateTime<Utc>>,
451    pub updated_at: DateTime<Utc>,
452}
453
454fn default_preview_content_format() -> LibraryPreviewContentFormat {
455    LibraryPreviewContentFormat::PlainText
456}
457
458fn default_text_content_format() -> LibraryTextContentFormat {
459    LibraryTextContentFormat::PlainText
460}
461
462fn default_metadata_json() -> Value {
463    json!({})
464}