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    pub filename: String,
108    pub media_type: String,
109    pub size_bytes: i64,
110    pub ingest_status: LibraryIngestStatus,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub error_message: Option<String>,
113    pub created_at: DateTime<Utc>,
114    pub updated_at: DateTime<Utc>,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub ingested_at: Option<DateTime<Utc>>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
120pub struct LibraryFolderNode {
121    pub group_key: String,
122    pub group_path: String,
123    pub visibility: Visibility,
124    #[serde(default)]
125    pub folder_id: Option<Uuid>,
126    #[serde(default)]
127    pub parent_folder_id: Option<Uuid>,
128    pub name: String,
129    pub path: String,
130    pub processing_count: usize,
131    #[schema(no_recursion)]
132    pub children: Vec<LibraryFolderNode>,
133    pub files: Vec<LibraryFileSummary>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
137pub struct LibraryFolderResponse {
138    pub folder_id: Uuid,
139    pub group_key: String,
140    pub group_path: String,
141    pub visibility: Visibility,
142    #[serde(default)]
143    pub parent_folder_id: Option<Uuid>,
144    pub name: String,
145    pub path: String,
146    pub created_at: DateTime<Utc>,
147    pub updated_at: DateTime<Utc>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
151pub struct LibraryTreeResponse {
152    pub root: LibraryFolderNode,
153}
154
155#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
156#[serde(rename_all = "snake_case")]
157pub enum LibraryResourceKind {
158    Folder,
159    File,
160}
161
162#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
163#[serde(rename_all = "snake_case")]
164pub enum LibraryResourceSortBy {
165    Name,
166    Type,
167    Status,
168    Size,
169    UpdatedAt,
170}
171
172impl LibraryResourceSortBy {
173    pub fn as_str(self) -> &'static str {
174        match self {
175            Self::Name => "name",
176            Self::Type => "type",
177            Self::Status => "status",
178            Self::Size => "size",
179            Self::UpdatedAt => "updated_at",
180        }
181    }
182}
183
184#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
185#[serde(rename_all = "snake_case")]
186pub enum SortDirection {
187    Asc,
188    Desc,
189}
190
191impl SortDirection {
192    pub fn as_str(self) -> &'static str {
193        match self {
194            Self::Asc => "asc",
195            Self::Desc => "desc",
196        }
197    }
198}
199
200fn default_page() -> u32 {
201    1
202}
203
204fn default_page_size() -> u32 {
205    50
206}
207
208fn default_resource_sort_by() -> LibraryResourceSortBy {
209    LibraryResourceSortBy::UpdatedAt
210}
211
212fn default_sort_direction() -> SortDirection {
213    SortDirection::Desc
214}
215
216#[derive(Debug, Clone, Deserialize, IntoParams)]
217#[into_params(parameter_in = Query)]
218pub struct LibraryResourcePageQuery {
219    #[serde(default)]
220    pub folder_id: Option<Uuid>,
221    #[serde(default = "default_page")]
222    pub page: u32,
223    #[serde(default = "default_page_size")]
224    pub page_size: u32,
225    #[serde(default)]
226    pub query: Option<String>,
227    #[serde(default)]
228    pub status: Option<LibraryIngestStatus>,
229    #[serde(default = "default_resource_sort_by")]
230    pub sort_by: LibraryResourceSortBy,
231    #[serde(default = "default_sort_direction")]
232    pub sort_direction: SortDirection,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
236pub struct LibraryResourceItem {
237    pub kind: LibraryResourceKind,
238    pub id: Uuid,
239    pub group_key: String,
240    pub group_path: String,
241    pub visibility: Visibility,
242    #[serde(default)]
243    pub parent_folder_id: Option<Uuid>,
244    pub name: String,
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub media_type: Option<String>,
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub size_bytes: Option<i64>,
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub ingest_status: Option<LibraryIngestStatus>,
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub error_message: Option<String>,
253    pub child_folder_count: u64,
254    pub file_count: u64,
255    pub processing_count: u64,
256    pub is_source_folder: bool,
257    pub is_source_records_folder: bool,
258    pub created_at: DateTime<Utc>,
259    pub updated_at: DateTime<Utc>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
263pub struct LibraryResourcePageResponse {
264    pub items: Vec<LibraryResourceItem>,
265    pub page: u32,
266    pub page_size: u32,
267    pub total: u64,
268    pub total_pages: u32,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
272pub struct LibraryIngestJobResponse {
273    pub job_id: Uuid,
274    pub group_key: String,
275    pub group_path: String,
276    pub visibility: Visibility,
277    pub file_id: Uuid,
278    pub status: LibraryIngestStatus,
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub docling_task_id: Option<String>,
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub error_message: Option<String>,
283    pub created_at: DateTime<Utc>,
284    #[serde(default, skip_serializing_if = "Option::is_none")]
285    pub started_at: Option<DateTime<Utc>>,
286    #[serde(default, skip_serializing_if = "Option::is_none")]
287    pub finished_at: Option<DateTime<Utc>>,
288    pub updated_at: DateTime<Utc>,
289}
290
291#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
292#[serde(rename_all = "snake_case")]
293pub enum LibraryTextContentFormat {
294    PlainText,
295    Markdown,
296}
297
298#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
299#[serde(rename_all = "snake_case")]
300pub enum LibraryPreviewContentFormat {
301    PlainText,
302    Markdown,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
306pub struct LibraryDocumentSectionPreview {
307    pub document_id: i64,
308    pub section_key: String,
309    pub section_label: String,
310    pub sort_order: i32,
311    pub title: String,
312    pub preview_text: String,
313    #[serde(default = "default_preview_content_format")]
314    pub content_format: LibraryPreviewContentFormat,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
318pub struct LibraryFileDetailResponse {
319    pub file_id: Uuid,
320    pub group_key: String,
321    pub group_path: String,
322    pub visibility: Visibility,
323    #[serde(default)]
324    pub folder_id: Option<Uuid>,
325    pub folder_path: String,
326    pub filename: String,
327    pub media_type: String,
328    pub size_bytes: i64,
329    pub sha256: String,
330    #[serde(default)]
331    pub source_available: bool,
332    pub ingest_status: LibraryIngestStatus,
333    #[serde(default, skip_serializing_if = "Option::is_none")]
334    pub error_message: Option<String>,
335    pub created_at: DateTime<Utc>,
336    pub updated_at: DateTime<Utc>,
337    #[serde(default, skip_serializing_if = "Option::is_none")]
338    pub ingested_at: Option<DateTime<Utc>>,
339    pub sections: Vec<LibraryDocumentSectionPreview>,
340    pub jobs: Vec<LibraryIngestJobResponse>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
344pub struct LibraryUploadResponse {
345    pub files: Vec<LibraryFileSummary>,
346    pub jobs: Vec<LibraryIngestJobResponse>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
350pub struct PrepareLibraryUploadRequest {
351    #[serde(default)]
352    pub folder_id: Option<Uuid>,
353    pub filename: String,
354    pub media_type: String,
355    pub size_bytes: i64,
356    pub sha256: String,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
360pub struct PrepareLibraryUploadResponse {
361    pub upload_required: bool,
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub file: Option<LibraryFileSummary>,
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub job: Option<LibraryIngestJobResponse>,
366}
367
368fn default_preview_content_format() -> LibraryPreviewContentFormat {
369    LibraryPreviewContentFormat::PlainText
370}
371
372fn default_text_content_format() -> LibraryTextContentFormat {
373    LibraryTextContentFormat::PlainText
374}
375
376fn default_metadata_json() -> Value {
377    json!({})
378}