1use crate::{
2 ProtocolVersion, PublicName,
3 asset::{AssetCatalog, SafeAssetCatalog},
4 pagination::PageRequest,
5 public_error::{ProtocolError, ValidationIssue, ValidationIssueCode},
6 refs::{AssetRef, DataFileRef, SessionSelector, StudyRef, TagSummary, VersionRef},
7 search::{PredicateSet, SearchCursorBinding, TagPredicate, TextPredicate},
8 study::StudySelector,
9 tabular::TabularDataRef,
10};
11
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
16pub enum DataFileSelector {
17 Id {
18 datafile: DataFileRef,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 study: Option<StudySelector>,
21 },
22 Name {
23 study: StudySelector,
24 name: PublicName,
25 },
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct DataFileVersionRef {
30 pub datafile: DataFileRef,
31 pub version: VersionRef,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct DataFileSummary {
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub version: Option<VersionRef>,
38 pub datafile: DataFileRef,
39 pub study: StudyRef,
40 pub name: String,
41 pub format: String,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub description: Option<String>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub size_bytes: Option<u64>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub digest_ref: Option<String>,
48 #[serde(default)]
49 pub tags: Vec<TagSummary>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct DataFileDetail {
54 pub summary: DataFileSummary,
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
56 pub tabular_refs: Vec<TabularDataRef>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct ListDataFilesRequest {
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub session: Option<SessionSelector>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub study: Option<StudySelector>,
65 pub page: PageRequest,
66 #[serde(default)]
67 pub include_versions: bool,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct GetDataFileMetadataRequest {
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub session: Option<SessionSelector>,
74 pub datafile: DataFileSelector,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub version: Option<String>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct ExportDataFileRequest {
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub session: Option<SessionSelector>,
83 pub datafile: DataFileSelector,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub version: Option<String>,
86 pub target: ExportTarget,
87 #[serde(default)]
88 pub overwrite: bool,
89 #[serde(default)]
90 pub compress: bool,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct ExportTarget {
95 pub path: String,
96 #[serde(default)]
97 pub request_only: bool,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct SearchDataFilesRequest {
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub session: Option<SessionSelector>,
104 pub predicates: PredicateSet<DataFilePredicate>,
105 pub page: PageRequest,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct BrowserDataFileSearchRequest {
113 pub datastore_id: crate::PublicUuid,
114 pub search: SearchDataFilesRequest,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct DataFileSearchCursorAnchor {
119 pub study: StudyRef,
120 pub datafile: DataFileRef,
121 pub name: String,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct DataFileSearchCursorQuery {
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub session: Option<SessionSelector>,
128 pub predicates: PredicateSet<DataFilePredicate>,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132pub struct DataFileSearchCursor {
133 pub binding: SearchCursorBinding<DataFileSearchCursorQuery>,
134 pub after: DataFileSearchCursorAnchor,
135}
136
137#[allow(clippy::result_large_err)]
138impl SearchDataFilesRequest {
139 pub fn validate(&mut self, protocol_version: &ProtocolVersion) -> Result<(), ProtocolError> {
140 let mut issues = Vec::new();
141 collect_predicate_set_issues("body.predicates", &self.predicates, &mut issues);
142 collect_datafile_predicate_issues(&mut self.predicates, &mut issues);
143 collect_page_issues("body.page.limit", self.page.limit, &mut issues);
144 collect_cursor_issues(self, protocol_version, &mut issues);
145
146 if issues.is_empty() {
147 Ok(())
148 } else {
149 Err(ProtocolError::validation(
150 "datafile search request is invalid",
151 issues,
152 ))
153 }
154 }
155
156 pub fn decoded_cursor(
157 &self,
158 protocol_version: &ProtocolVersion,
159 ) -> Result<Option<DataFileSearchCursorAnchor>, ProtocolError> {
160 match self.page.cursor.as_deref() {
161 None => Ok(None),
162 Some(raw_cursor) => {
163 let cursor: DataFileSearchCursor =
164 serde_json::from_str(raw_cursor).map_err(|error| {
165 ProtocolError::validation(
166 "datafile search request is invalid",
167 vec![ValidationIssue::new(
168 "body.page.cursor",
169 ValidationIssueCode::InvalidFormat,
170 format!("datafile search cursor is invalid: {error}"),
171 )],
172 )
173 })?;
174 let expected = self.cursor_binding(protocol_version);
175 if cursor.binding != expected {
176 return Err(ProtocolError::validation(
177 "datafile search request is invalid",
178 vec![ValidationIssue::new(
179 "body.page.cursor",
180 ValidationIssueCode::Conflict,
181 "datafile search cursor does not match the request predicates, page limit, or protocol version",
182 )],
183 ));
184 }
185 Ok(Some(cursor.after))
186 }
187 }
188 }
189
190 pub fn encode_cursor(
191 &self,
192 protocol_version: &ProtocolVersion,
193 after: DataFileSearchCursorAnchor,
194 ) -> Result<String, ProtocolError> {
195 serde_json::to_string(&DataFileSearchCursor {
196 binding: self.cursor_binding(protocol_version),
197 after,
198 })
199 .map_err(|error| {
200 ProtocolError::new(
201 crate::public_error::ProtocolErrorCode::Internal,
202 format!("failed to encode datafile search cursor: {error}"),
203 )
204 })
205 }
206
207 fn cursor_binding(
208 &self,
209 protocol_version: &ProtocolVersion,
210 ) -> SearchCursorBinding<DataFileSearchCursorQuery> {
211 SearchCursorBinding {
212 query: DataFileSearchCursorQuery {
213 session: self.session.clone(),
214 predicates: self.predicates.clone(),
215 },
216 page_limit: self.page.limit,
217 protocol_version: protocol_version.to_string(),
218 }
219 }
220
221 #[allow(clippy::result_large_err)]
225 pub fn validate_for_browser(
226 &mut self,
227 datastore_id: crate::PublicUuid,
228 protocol_version: &ProtocolVersion,
229 ) -> Result<(), ProtocolError> {
230 let mut issues = Vec::new();
231 if self.session.is_some() {
232 issues.push(ValidationIssue::new(
233 "body.session",
234 ValidationIssueCode::Unsupported,
235 "daemon session is not accepted for browser datafile search",
236 ));
237 }
238 collect_predicate_set_issues("body.predicates", &self.predicates, &mut issues);
239 collect_datafile_predicate_issues(&mut self.predicates, &mut issues);
240 collect_page_issues("body.page.limit", self.page.limit, &mut issues);
241 if let Some(raw_cursor) = self.page.cursor.as_deref() {
242 match serde_json::from_str::<BrowserDataFileSearchCursor>(raw_cursor) {
243 Ok(cursor)
244 if cursor.binding
245 == self.browser_cursor_binding(datastore_id, protocol_version) => {}
246 Ok(_) => issues.push(ValidationIssue::new(
247 "body.page.cursor",
248 ValidationIssueCode::Conflict,
249 "browser datafile search cursor does not match the datastore, search family, predicates, page limit, or protocol version",
250 )),
251 Err(_) => issues.push(ValidationIssue::new(
252 "body.page.cursor",
253 ValidationIssueCode::InvalidFormat,
254 "browser datafile search cursor is invalid",
255 )),
256 }
257 }
258 if issues.is_empty() {
259 Ok(())
260 } else {
261 Err(ProtocolError::validation(
262 "browser datafile search request is invalid",
263 issues,
264 ))
265 }
266 }
267
268 #[allow(clippy::result_large_err)]
269 pub fn browser_decoded_cursor(
270 &self,
271 datastore_id: crate::PublicUuid,
272 protocol_version: &ProtocolVersion,
273 ) -> Result<Option<BrowserDataFileSearchCursorAnchor>, ProtocolError> {
274 let Some(raw_cursor) = self.page.cursor.as_deref() else {
275 return Ok(None);
276 };
277 let cursor: BrowserDataFileSearchCursor =
278 serde_json::from_str(raw_cursor).map_err(|_| {
279 ProtocolError::validation(
280 "browser datafile search request is invalid",
281 vec![ValidationIssue::new(
282 "body.page.cursor",
283 ValidationIssueCode::InvalidFormat,
284 "browser datafile search cursor is invalid",
285 )],
286 )
287 })?;
288 if cursor.binding != self.browser_cursor_binding(datastore_id, protocol_version) {
289 return Err(ProtocolError::validation(
290 "browser datafile search request is invalid",
291 vec![ValidationIssue::new(
292 "body.page.cursor",
293 ValidationIssueCode::Conflict,
294 "browser datafile search cursor does not match the datastore, search family, predicates, page limit, or protocol version",
295 )],
296 ));
297 }
298 Ok(Some(cursor.after))
299 }
300
301 #[allow(clippy::result_large_err)]
302 pub fn encode_browser_cursor(
303 &self,
304 datastore_id: crate::PublicUuid,
305 protocol_version: &ProtocolVersion,
306 after: BrowserDataFileSearchCursorAnchor,
307 ) -> Result<String, ProtocolError> {
308 serde_json::to_string(&BrowserDataFileSearchCursor {
309 binding: self.browser_cursor_binding(datastore_id, protocol_version),
310 after,
311 })
312 .map_err(|_| {
313 ProtocolError::new(
314 crate::public_error::ProtocolErrorCode::Internal,
315 "failed to encode browser datafile search cursor",
316 )
317 })
318 }
319
320 fn browser_cursor_binding(
321 &self,
322 datastore_id: crate::PublicUuid,
323 protocol_version: &ProtocolVersion,
324 ) -> SearchCursorBinding<BrowserDataFileSearchCursorQuery> {
325 SearchCursorBinding {
326 query: BrowserDataFileSearchCursorQuery {
327 datastore_id,
328 family: "datafiles".to_string(),
329 predicates: self.predicates.clone(),
330 },
331 page_limit: self.page.limit,
332 protocol_version: protocol_version.to_string(),
333 }
334 }
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub struct BrowserDataFileSearchResponse {
343 pub protocol_version: ProtocolVersion,
344 pub datastore_id: crate::PublicUuid,
345 pub family: String,
346 pub predicates: PredicateSet<DataFilePredicate>,
347 #[serde(default)]
348 pub datafiles: Vec<crate::resource_discovery::BrowserDataFileDiscoveryRecord>,
349 pub returned_count: u16,
350 pub requested_limit: u16,
351 pub ordering: crate::study::BrowserSearchOrdering,
352 pub has_more: bool,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub next_cursor: Option<String>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub empty_state: Option<BrowserDataFileSearchEmptyState>,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
360pub struct BrowserDataFileSearchEmptyState {
361 pub code: String,
362 pub message: String,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
366pub struct BrowserDataFileSearchCursorQuery {
367 pub datastore_id: crate::PublicUuid,
368 pub family: String,
369 pub predicates: PredicateSet<DataFilePredicate>,
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373pub struct BrowserDataFileSearchCursor {
374 pub binding: SearchCursorBinding<BrowserDataFileSearchCursorQuery>,
375 pub after: BrowserDataFileSearchCursorAnchor,
376}
377
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
379pub struct BrowserDataFileSearchCursorAnchor {
380 pub study: StudyRef,
381 pub datafile: DataFileRef,
382 pub asset: AssetRef,
383 pub name: String,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
387#[serde(tag = "kind", rename_all = "snake_case")]
388pub enum DataFilePredicate {
389 Text(TextPredicate<DataFileTextField>),
390 Study { study: StudySelector },
391 Format { format: String },
392 Tag(TagPredicate),
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
396#[serde(rename_all = "snake_case")]
397pub enum DataFileTextField {
398 Name,
399 Description,
400 Format,
401}
402
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct DataFileEntryPayload {
405 pub catalog: SafeAssetCatalog,
406 pub datafiles: Vec<SafeDataFileVersionSummary>,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
414pub struct SafeDataFileVersionSummary {
415 pub datafile_id: VersionRef,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub compressed: Option<bool>,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub encrypted: Option<bool>,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub compression_algorithm: Option<String>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub encryption_algorithm: Option<String>,
424 pub edam_format: String,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428pub struct DataFileListResponse {
429 pub session: crate::session::SessionStatusPayload,
430 pub study: String,
431 pub study_id: StudyRef,
432 pub datafiles: Vec<DataFileEntryPayload>,
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct DataFileSearchResponse {
437 pub session: crate::session::SessionStatusPayload,
438 pub datafiles: Vec<DataFileSummary>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub next_cursor: Option<String>,
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
444pub struct DataFileMetadataPayload {
445 pub catalog: SafeAssetCatalog,
446 pub version: crate::asset::SafeAssetVersionSummary,
447 pub datafile: SafeDataFileVersionSummary,
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451pub struct DataFileMetadataResponse {
452 pub session: crate::session::SessionStatusPayload,
453 pub study: String,
454 pub metadata: DataFileMetadataPayload,
455}
456
457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
458pub struct DataFileExportResponse {
459 pub session: crate::session::SessionStatusPayload,
460 pub study: String,
461 pub catalog: AssetCatalog,
462 pub datafile: DataFileSummary,
463 pub lineage: crate::transformation::TransformationSummary,
464 pub artifact: ExportedArtifactSummary,
465 #[serde(default, skip_serializing_if = "Vec::is_empty")]
466 pub warnings: Vec<String>,
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
470pub struct ExportedArtifactSummary {
471 pub kind: ExportArtifactKind,
472 pub format: String,
473 pub compressed: bool,
474 pub stored_size_bytes: u64,
475 pub logical_size_bytes: u64,
476 pub exported_size_bytes: u64,
477 #[serde(default)]
478 pub target_request_only: bool,
479}
480
481#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
482#[serde(rename_all = "snake_case")]
483pub enum ExportArtifactKind {
484 DataFile,
485}
486
487fn collect_predicate_set_issues<P>(
488 path: &str,
489 predicates: &PredicateSet<P>,
490 issues: &mut Vec<ValidationIssue>,
491) {
492 if let Err(message) = predicates.validate() {
493 issues.push(ValidationIssue::new(
494 path,
495 ValidationIssueCode::InvalidValue,
496 message,
497 ));
498 }
499}
500
501fn collect_datafile_predicate_issues(
502 predicates: &mut PredicateSet<DataFilePredicate>,
503 issues: &mut Vec<ValidationIssue>,
504) {
505 for (index, predicate) in predicates.predicates_mut().iter_mut().enumerate() {
506 match predicate {
507 DataFilePredicate::Text(text) => {
508 if let Err(message) = text.validate() {
509 issues.push(ValidationIssue::new(
510 format!("body.predicates.predicates[{index}].value"),
511 ValidationIssueCode::InvalidValue,
512 message,
513 ));
514 }
515 }
516 DataFilePredicate::Format { format } => {
517 *format = format.trim().to_string();
518 if format.is_empty() {
519 issues.push(ValidationIssue::new(
520 format!("body.predicates.predicates[{index}].format"),
521 ValidationIssueCode::TooShort,
522 "datafile format must not be empty",
523 ));
524 } else if format.len() > crate::search::MAX_TEXT_VALUE_LENGTH {
525 issues.push(ValidationIssue::new(
526 format!("body.predicates.predicates[{index}].format"),
527 ValidationIssueCode::TooLong,
528 format!(
529 "datafile format must be at most {} characters",
530 crate::search::MAX_TEXT_VALUE_LENGTH
531 ),
532 ));
533 }
534 }
535 DataFilePredicate::Study { .. } | DataFilePredicate::Tag(_) => {}
536 }
537 }
538}
539
540fn collect_page_issues(path: &str, limit: u16, issues: &mut Vec<ValidationIssue>) {
541 if limit == 0 || limit > crate::search::MAX_PAGE_LIMIT {
542 issues.push(ValidationIssue::new(
543 path,
544 ValidationIssueCode::OutOfRange,
545 if limit == 0 {
546 "page limit must be greater than zero".to_string()
547 } else {
548 format!(
549 "page limit must be at most {}",
550 crate::search::MAX_PAGE_LIMIT
551 )
552 },
553 ));
554 }
555}
556
557fn collect_cursor_issues(
558 request: &SearchDataFilesRequest,
559 protocol_version: &ProtocolVersion,
560 issues: &mut Vec<ValidationIssue>,
561) {
562 if let Some(raw_cursor) = request.page.cursor.as_deref() {
563 let cursor: DataFileSearchCursor = match serde_json::from_str(raw_cursor) {
564 Ok(cursor) => cursor,
565 Err(error) => {
566 issues.push(ValidationIssue::new(
567 "body.page.cursor",
568 ValidationIssueCode::InvalidFormat,
569 format!("datafile search cursor is invalid: {error}"),
570 ));
571 return;
572 }
573 };
574
575 if cursor.binding != request.cursor_binding(protocol_version) {
576 issues.push(ValidationIssue::new(
577 "body.page.cursor",
578 ValidationIssueCode::Conflict,
579 "datafile search cursor does not match the request predicates, page limit, or protocol version",
580 ));
581 }
582 }
583}