Skip to main content

client_uploader_traits/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![deny(missing_docs)]
4#![deny(
5    clippy::all,
6    clippy::cargo,
7    clippy::pedantic,
8    clippy::expect_used,
9    clippy::missing_errors_doc,
10    clippy::missing_panics_doc,
11    clippy::panic,
12    clippy::todo,
13    clippy::unimplemented,
14    clippy::unwrap_used
15)]
16#![allow(clippy::module_name_repetitions)]
17
18use std::collections::BTreeSet;
19use std::error::Error as StdError;
20use std::fmt;
21use std::future::Future;
22use std::path::Path;
23use std::time::Duration;
24
25use url::Url;
26
27/// Marker used when a service creates new resources without a caller-supplied target identifier.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
29pub struct NoCreateTarget;
30
31/// Shared classification of upload input sources.
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
33pub enum UploadSourceKind {
34    /// Upload bytes come from a local file path.
35    Path,
36    /// Upload bytes come from a reader or stream-like source.
37    Reader,
38    /// Upload bytes are already materialized in memory.
39    Bytes,
40}
41
42/// Normalized draft-file reconciliation semantics shared by Zenodo and Figshare.
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
44pub enum DraftFilePolicyKind {
45    /// Remove all existing files before publishing the new upload set.
46    ReplaceAll,
47    /// Replace only the files whose names match one of the new uploads.
48    UpsertByFilename,
49    /// Keep existing files and add the new uploads alongside them.
50    KeepExistingAndAdd,
51}
52
53/// Normalized per-file conflict semantics shared by direct-upload services such as Internet Archive.
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
55pub enum ExistingFileConflictPolicyKind {
56    /// Fail when a file with the same name already exists.
57    Error,
58    /// Skip uploads whose names already exist.
59    Skip,
60    /// Replace the existing file in place.
61    Overwrite,
62    /// Replace the existing file while preserving history on the remote service.
63    OverwriteKeepingHistory,
64}
65
66/// Generic request for “create a brand-new published resource”.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct CreatePublicationRequest<Target, Metadata, Upload> {
69    /// Caller-supplied creation target.
70    ///
71    /// Use [`NoCreateTarget`] for services that create new resources without an
72    /// external identifier.
73    pub target: Target,
74    /// Descriptive metadata to apply before publication.
75    pub metadata: Metadata,
76    /// Files to upload as part of the publication workflow.
77    pub uploads: Vec<Upload>,
78}
79
80impl<Target, Metadata, Upload> CreatePublicationRequest<Target, Metadata, Upload> {
81    /// Creates a new request from explicit parts.
82    #[must_use]
83    pub fn new(target: Target, metadata: Metadata, uploads: Vec<Upload>) -> Self {
84        Self {
85            target,
86            metadata,
87            uploads,
88        }
89    }
90
91    /// Appends one upload to the request.
92    #[must_use]
93    pub fn with_upload(mut self, upload: Upload) -> Self {
94        self.uploads.push(upload);
95        self
96    }
97
98    /// Maps the upload list into another upload type while preserving target and metadata.
99    #[must_use]
100    pub fn map_uploads<MappedUpload>(
101        self,
102        mut map: impl FnMut(Upload) -> MappedUpload,
103    ) -> CreatePublicationRequest<Target, Metadata, MappedUpload> {
104        CreatePublicationRequest {
105            target: self.target,
106            metadata: self.metadata,
107            uploads: self.uploads.into_iter().map(&mut map).collect(),
108        }
109    }
110}
111
112impl<Metadata, Upload> CreatePublicationRequest<NoCreateTarget, Metadata, Upload> {
113    /// Creates a new request for services that do not require a caller-supplied creation target.
114    #[must_use]
115    pub fn untargeted(metadata: Metadata, uploads: Vec<Upload>) -> Self {
116        Self::new(NoCreateTarget, metadata, uploads)
117    }
118}
119
120/// Generic request for “update or upsert an existing published resource”.
121#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct UpdatePublicationRequest<ResourceId, Metadata, Policy, Upload> {
123    /// Identifier of the existing resource family or mutable root object.
124    pub resource_id: ResourceId,
125    /// Descriptive metadata to apply during the workflow.
126    pub metadata: Metadata,
127    /// Upload reconciliation or conflict policy.
128    pub policy: Policy,
129    /// Files to upload as part of the workflow.
130    pub uploads: Vec<Upload>,
131}
132
133impl<ResourceId, Metadata, Policy, Upload>
134    UpdatePublicationRequest<ResourceId, Metadata, Policy, Upload>
135{
136    /// Creates a new request from explicit parts.
137    #[must_use]
138    pub fn new(
139        resource_id: ResourceId,
140        metadata: Metadata,
141        policy: Policy,
142        uploads: Vec<Upload>,
143    ) -> Self {
144        Self {
145            resource_id,
146            metadata,
147            policy,
148            uploads,
149        }
150    }
151
152    /// Appends one upload to the request.
153    #[must_use]
154    pub fn with_upload(mut self, upload: Upload) -> Self {
155        self.uploads.push(upload);
156        self
157    }
158
159    /// Maps the upload list into another upload type while preserving identifiers, metadata, and policy.
160    #[must_use]
161    pub fn map_uploads<MappedUpload>(
162        self,
163        mut map: impl FnMut(Upload) -> MappedUpload,
164    ) -> UpdatePublicationRequest<ResourceId, Metadata, Policy, MappedUpload> {
165        UpdatePublicationRequest {
166            resource_id: self.resource_id,
167            metadata: self.metadata,
168            policy: self.policy,
169            uploads: self.uploads.into_iter().map(&mut map).collect(),
170        }
171    }
172}
173
174/// Validation error returned by shared upload-name helpers.
175#[derive(Clone, Debug, PartialEq, Eq)]
176pub enum UploadNameValidationError {
177    /// One upload had an empty remote filename.
178    EmptyFilename,
179    /// More than one upload targeted the same remote filename.
180    DuplicateFilename {
181        /// The duplicated filename.
182        filename: String,
183    },
184}
185
186impl fmt::Display for UploadNameValidationError {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        match self {
189            Self::EmptyFilename => f.write_str("upload filename cannot be empty"),
190            Self::DuplicateFilename { filename } => {
191                write!(f, "duplicate upload filename: {filename}")
192            }
193        }
194    }
195}
196
197impl StdError for UploadNameValidationError {}
198
199/// Minimal configuration surface shared by the uploader clients.
200pub trait ClientContext {
201    /// Service-specific endpoint configuration type.
202    type Endpoint;
203    /// Service-specific polling configuration type.
204    type PollOptions;
205    /// Service-specific error type.
206    type Error;
207
208    /// Returns the configured endpoint roots.
209    fn endpoint(&self) -> &Self::Endpoint;
210
211    /// Returns the configured polling behavior.
212    fn poll_options(&self) -> &Self::PollOptions;
213
214    /// Returns the overall request timeout, when configured.
215    fn request_timeout(&self) -> Option<Duration>;
216
217    /// Returns the TCP connect timeout, when configured.
218    fn connect_timeout(&self) -> Option<Duration>;
219}
220
221/// Shared auth-state inspection surface.
222pub trait MaybeAuthenticatedClient: ClientContext {
223    /// Returns whether the client currently has credentials available for authenticated calls.
224    fn has_auth(&self) -> bool;
225}
226
227/// Shared inspection surface for upload specifications.
228pub trait UploadSpecLike {
229    /// Returns the remote filename that the upload will expose.
230    fn filename(&self) -> &str;
231
232    /// Returns the kind of source backing this upload.
233    fn source_kind(&self) -> UploadSourceKind;
234
235    /// Returns the exact content length when it is already known cheaply.
236    fn content_length(&self) -> Option<u64> {
237        None
238    }
239
240    /// Returns the HTTP content type when the upload tracks one.
241    fn content_type(&self) -> Option<&str> {
242        None
243    }
244}
245
246/// Shared inspection surface for remote file entries.
247pub trait RepositoryFile {
248    /// Service-specific file identifier type.
249    type Id: Clone;
250
251    /// Returns the file identifier when the service exposes one.
252    fn file_id(&self) -> Option<Self::Id>;
253
254    /// Returns the remote filename or key.
255    fn file_name(&self) -> &str;
256
257    /// Returns the file size in bytes when known.
258    fn size_bytes(&self) -> Option<u64>;
259
260    /// Returns a checksum string when the service reports one.
261    fn checksum(&self) -> Option<&str> {
262        None
263    }
264
265    /// Returns the best direct download URL when one is already present on the payload.
266    fn download_url(&self) -> Option<&Url> {
267        None
268    }
269}
270
271/// Shared inspection surface for top-level resource payloads.
272///
273/// This trait intentionally treats embedded file lists as advisory. Some
274/// services, notably Figshare, may return partial embedded file lists on the
275/// main resource payload. Use [`ListResourceFiles`] when complete enumeration
276/// matters.
277pub trait RepositoryRecord {
278    /// Service-specific resource identifier type.
279    type Id: Clone;
280    /// Service-specific file entry type.
281    type File: RepositoryFile;
282
283    /// Returns the top-level resource identifier when it can be read directly from the payload.
284    fn resource_id(&self) -> Option<Self::Id>;
285
286    /// Returns a display title when one is available.
287    fn title(&self) -> Option<&str>;
288
289    /// Returns the embedded file list on the payload.
290    fn files(&self) -> &[Self::File];
291}
292
293/// Shared inspection surface for DOI-backed resources.
294pub trait DoiBackedRecord {
295    /// Service-specific DOI type.
296    type Doi: Clone;
297
298    /// Returns the resource DOI when one is present.
299    fn doi(&self) -> Option<Self::Doi>;
300}
301
302/// Shared inspection surface for mutable draft-like resources.
303pub trait DraftResource {
304    /// Service-specific draft identifier type.
305    type Id: Clone;
306    /// Service-specific draft file entry type.
307    type File: RepositoryFile;
308
309    /// Returns the draft identifier.
310    fn draft_id(&self) -> Self::Id;
311
312    /// Returns the embedded draft file list.
313    fn files(&self) -> &[Self::File];
314}
315
316/// Shared inspection surface for publication workflow results.
317pub trait PublicationOutcome {
318    /// Public resource type returned by the workflow.
319    type PublicResource;
320
321    /// Returns the final public resource visible after the workflow completes.
322    fn public_resource(&self) -> &Self::PublicResource;
323
324    /// Returns whether the workflow definitely created a brand-new resource.
325    ///
326    /// `None` means the workflow result does not carry this distinction.
327    fn created(&self) -> Option<bool> {
328        None
329    }
330}
331
332/// Shared inspection surface for publication workflows that also return a mutable or private-side resource.
333pub trait MutablePublicationOutcome: PublicationOutcome {
334    /// Mutable or private-side resource type returned by the workflow.
335    type MutableResource;
336
337    /// Returns the mutable or private-side resource when the workflow exposes one.
338    fn mutable_resource(&self) -> Option<&Self::MutableResource>;
339}
340
341/// Shared publication-state inspection for mutable resources.
342pub trait DraftState {
343    /// Returns whether the remote object is already published.
344    fn is_published(&self) -> bool;
345
346    /// Returns whether metadata edits are currently allowed.
347    fn allows_metadata_updates(&self) -> bool;
348}
349
350/// Capability for reading one public resource by its primary service identifier.
351pub trait ReadPublicResource: ClientContext {
352    /// Service-specific resource identifier type.
353    type ResourceId;
354    /// Returned public resource payload.
355    type Resource;
356
357    /// Fetches one public resource.
358    fn get_public_resource(
359        &self,
360        id: &Self::ResourceId,
361    ) -> impl Future<Output = Result<Self::Resource, Self::Error>>;
362}
363
364/// Capability for searching public resources.
365pub trait SearchPublicResources: ClientContext {
366    /// Service-specific query type.
367    type Query;
368    /// Service-specific search result shape.
369    type SearchResults;
370
371    /// Executes a public search request.
372    fn search_public_resources(
373        &self,
374        query: &Self::Query,
375    ) -> impl Future<Output = Result<Self::SearchResults, Self::Error>>;
376}
377
378/// Shared inspection surface for search result containers.
379pub trait SearchResultsLike {
380    /// One result item.
381    type Item;
382
383    /// Returns the items materialized on the current result page.
384    fn items(&self) -> &[Self::Item];
385
386    /// Returns the total hit count when the backend reports one separately from the current page.
387    fn total_hits(&self) -> Option<u64> {
388        None
389    }
390
391    /// Returns the number of items on the current page.
392    #[must_use]
393    fn page_len(&self) -> usize {
394        self.items().len()
395    }
396
397    /// Returns whether the current result page has no items.
398    #[must_use]
399    fn is_empty(&self) -> bool {
400        self.items().is_empty()
401    }
402}
403
404impl<T> SearchResultsLike for Vec<T> {
405    type Item = T;
406
407    fn items(&self) -> &[Self::Item] {
408        self.as_slice()
409    }
410}
411
412/// Capability for enumerating files attached to a public resource.
413pub trait ListResourceFiles: ClientContext {
414    /// Service-specific resource identifier type.
415    type ResourceId;
416    /// Service-specific file entry type.
417    type File;
418
419    /// Returns the complete visible file list for the resource.
420    fn list_resource_files(
421        &self,
422        id: &Self::ResourceId,
423    ) -> impl Future<Output = Result<Vec<Self::File>, Self::Error>>;
424}
425
426/// Capability for downloading one named public file to a local path.
427pub trait DownloadNamedPublicFile: ClientContext {
428    /// Service-specific resource identifier type.
429    type ResourceId;
430    /// Service-specific download result metadata.
431    type Download;
432
433    /// Downloads one named file from a public resource.
434    fn download_named_public_file_to_path(
435        &self,
436        id: &Self::ResourceId,
437        name: &str,
438        path: &Path,
439    ) -> impl Future<Output = Result<Self::Download, Self::Error>>;
440}
441
442/// Capability for creating and publishing a brand-new resource.
443pub trait CreatePublication: ClientContext {
444    /// Caller-supplied creation target type.
445    type CreateTarget;
446    /// Service-specific metadata type.
447    type Metadata;
448    /// Service-specific upload specification type.
449    type Upload;
450    /// Service-specific workflow result type.
451    type Output;
452
453    /// Runs the service’s “create a new published resource” workflow.
454    fn create_publication(
455        &self,
456        request: CreatePublicationRequest<Self::CreateTarget, Self::Metadata, Self::Upload>,
457    ) -> impl Future<Output = Result<Self::Output, Self::Error>>;
458}
459
460/// Capability for updating or upserting an existing published resource.
461pub trait UpdatePublication: ClientContext {
462    /// Service-specific resource identifier type.
463    type ResourceId;
464    /// Service-specific metadata type.
465    type Metadata;
466    /// Service-specific upload-policy type.
467    type FilePolicy;
468    /// Service-specific upload specification type.
469    type Upload;
470    /// Service-specific workflow result type.
471    type Output;
472
473    /// Runs the service’s “update or upsert an existing resource” workflow.
474    fn update_publication(
475        &self,
476        request: UpdatePublicationRequest<
477            Self::ResourceId,
478            Self::Metadata,
479            Self::FilePolicy,
480            Self::Upload,
481        >,
482    ) -> impl Future<Output = Result<Self::Output, Self::Error>>;
483}
484
485/// DOI-based lookup for public resources.
486pub trait LookupByDoi: ClientContext {
487    /// Service-specific DOI type.
488    type Doi;
489    /// Returned public resource payload.
490    type Resource;
491
492    /// Fetches one public resource by DOI.
493    fn get_public_resource_by_doi(
494        &self,
495        doi: &Self::Doi,
496    ) -> impl Future<Output = Result<Self::Resource, Self::Error>>;
497}
498
499/// Latest-version resolution for public resources that expose version families.
500pub trait ResolveLatestPublicResource: ClientContext {
501    /// Service-specific resource identifier type.
502    type ResourceId;
503    /// Returned public resource payload.
504    type Resource;
505
506    /// Resolves the latest public version in the resource family.
507    fn resolve_latest_public_resource(
508        &self,
509        id: &Self::ResourceId,
510    ) -> impl Future<Output = Result<Self::Resource, Self::Error>>;
511}
512
513/// Latest-version resolution for DOI-backed public resources.
514pub trait ResolveLatestPublicResourceByDoi: ClientContext {
515    /// Service-specific DOI type.
516    type Doi;
517    /// Returned public resource payload.
518    type Resource;
519
520    /// Resolves the latest public version in the DOI-backed resource family.
521    fn resolve_latest_public_resource_by_doi(
522        &self,
523        doi: &Self::Doi,
524    ) -> impl Future<Output = Result<Self::Resource, Self::Error>>;
525}
526
527/// Shared classification trait for Zenodo/Figshare draft reconciliation policies.
528pub trait DraftFilePolicy {
529    /// Returns the normalized reconciliation mode.
530    fn kind(&self) -> DraftFilePolicyKind;
531}
532
533/// Shared classification trait for Internet Archive-style per-file conflict policies.
534pub trait ExistingFileConflictPolicy {
535    /// Returns the normalized conflict mode.
536    fn kind(&self) -> ExistingFileConflictPolicyKind;
537}
538
539/// Lower-level mutable-draft workflow shared most directly by Zenodo and Figshare.
540pub trait DraftWorkflow: ClientContext {
541    /// Service-specific mutable draft type.
542    type Draft: DraftResource;
543    /// Service-specific metadata type.
544    type Metadata;
545    /// Service-specific upload specification type.
546    type Upload;
547    /// Service-specific draft file policy.
548    type FilePolicy;
549    /// Service-specific upload result type.
550    type UploadResult;
551    /// Result returned by the publish step.
552    type Published;
553
554    /// Creates a new mutable draft-like resource with the supplied metadata.
555    fn create_draft(
556        &self,
557        metadata: &Self::Metadata,
558    ) -> impl Future<Output = Result<Self::Draft, Self::Error>>;
559
560    /// Replaces or merges the draft metadata.
561    fn update_draft_metadata(
562        &self,
563        draft_id: &<Self::Draft as DraftResource>::Id,
564        metadata: &Self::Metadata,
565    ) -> impl Future<Output = Result<Self::Draft, Self::Error>>;
566
567    /// Reconciles the draft file set under the requested policy.
568    fn reconcile_draft_files(
569        &self,
570        draft: &Self::Draft,
571        policy: Self::FilePolicy,
572        uploads: Vec<Self::Upload>,
573    ) -> impl Future<Output = Result<Vec<Self::UploadResult>, Self::Error>>;
574
575    /// Publishes the draft-like resource.
576    fn publish_draft(
577        &self,
578        draft_id: &<Self::Draft as DraftResource>::Id,
579    ) -> impl Future<Output = Result<Self::Published, Self::Error>>;
580}
581
582/// Bundle for the capability set shared by all three current uploader clients.
583pub trait CoreRepositoryClient:
584    ClientContext
585    + CreatePublication
586    + DownloadNamedPublicFile
587    + ListResourceFiles
588    + ReadPublicResource
589    + SearchPublicResources
590    + UpdatePublication
591{
592}
593
594impl<T> CoreRepositoryClient for T where
595    T: ClientContext
596        + CreatePublication
597        + DownloadNamedPublicFile
598        + ListResourceFiles
599        + ReadPublicResource
600        + SearchPublicResources
601        + UpdatePublication
602{
603}
604
605/// Bundle for clients that additionally support DOI lookup and latest-version resolution.
606pub trait DoiVersionedRepositoryClient:
607    CoreRepositoryClient + LookupByDoi + ResolveLatestPublicResource + ResolveLatestPublicResourceByDoi
608{
609}
610
611impl<T> DoiVersionedRepositoryClient for T where
612    T: CoreRepositoryClient
613        + LookupByDoi
614        + ResolveLatestPublicResource
615        + ResolveLatestPublicResourceByDoi
616{
617}
618
619/// Bundle for clients that expose draft-oriented mutation workflows.
620pub trait DraftPublishingRepositoryClient: CoreRepositoryClient + DraftWorkflow {}
621
622impl<T> DraftPublishingRepositoryClient for T where T: CoreRepositoryClient + DraftWorkflow {}
623
624/// Returns the first file whose name matches `name`.
625#[must_use]
626pub fn find_file_by_name<'a, File>(
627    files: impl IntoIterator<Item = &'a File>,
628    name: &str,
629) -> Option<&'a File>
630where
631    File: RepositoryFile + ?Sized + 'a,
632{
633    files.into_iter().find(|file| file.file_name() == name)
634}
635
636/// Returns whether any file in the iterator matches `name`.
637#[must_use]
638pub fn has_file_named<'a, File>(files: impl IntoIterator<Item = &'a File>, name: &str) -> bool
639where
640    File: RepositoryFile + ?Sized + 'a,
641{
642    find_file_by_name(files, name).is_some()
643}
644
645/// Returns the first embedded file on a record payload whose name matches `name`.
646#[must_use]
647pub fn find_embedded_record_file_by_name<'a, Record>(
648    record: &'a Record,
649    name: &str,
650) -> Option<&'a Record::File>
651where
652    Record: RepositoryRecord + ?Sized,
653{
654    find_file_by_name(record.files().iter(), name)
655}
656
657/// Returns the first embedded file on a draft payload whose name matches `name`.
658#[must_use]
659pub fn find_embedded_draft_file_by_name<'a, Draft>(
660    draft: &'a Draft,
661    name: &str,
662) -> Option<&'a Draft::File>
663where
664    Draft: DraftResource + ?Sized,
665{
666    find_file_by_name(draft.files().iter(), name)
667}
668
669/// Collects the set of upload filenames while validating that names are non-empty and unique.
670///
671/// The returned set can be reused for draft-reconciliation and conflict checks.
672///
673/// # Errors
674///
675/// Returns [`UploadNameValidationError::EmptyFilename`] when one upload has an
676/// empty remote filename, or [`UploadNameValidationError::DuplicateFilename`]
677/// when multiple uploads target the same remote filename.
678pub fn collect_upload_filenames<'a, Upload>(
679    uploads: impl IntoIterator<Item = &'a Upload>,
680) -> Result<BTreeSet<String>, UploadNameValidationError>
681where
682    Upload: UploadSpecLike + ?Sized + 'a,
683{
684    let mut filenames = BTreeSet::new();
685    for upload in uploads {
686        let filename = upload.filename();
687        if filename.is_empty() {
688            return Err(UploadNameValidationError::EmptyFilename);
689        }
690        if !filenames.insert(filename.to_owned()) {
691            return Err(UploadNameValidationError::DuplicateFilename {
692                filename: filename.to_owned(),
693            });
694        }
695    }
696    Ok(filenames)
697}
698
699/// Validates that upload filenames are non-empty and unique.
700///
701/// # Errors
702///
703/// Returns the same validation errors as [`collect_upload_filenames`].
704pub fn validate_upload_filenames<'a, Upload>(
705    uploads: impl IntoIterator<Item = &'a Upload>,
706) -> Result<(), UploadNameValidationError>
707where
708    Upload: UploadSpecLike + ?Sized + 'a,
709{
710    collect_upload_filenames(uploads).map(|_| ())
711}
712
713/// Returns whether any upload in the iterator targets `filename`.
714#[must_use]
715pub fn has_upload_filename<'a, Upload>(
716    uploads: impl IntoIterator<Item = &'a Upload>,
717    filename: &str,
718) -> bool
719where
720    Upload: UploadSpecLike + ?Sized + 'a,
721{
722    uploads
723        .into_iter()
724        .any(|upload| upload.filename() == filename)
725}
726
727/// Common re-exports for crate consumers and implementers.
728pub mod prelude {
729    pub use crate::{
730        collect_upload_filenames, find_embedded_draft_file_by_name,
731        find_embedded_record_file_by_name, find_file_by_name, has_file_named, has_upload_filename,
732        validate_upload_filenames, ClientContext, CoreRepositoryClient, CreatePublication,
733        CreatePublicationRequest, DoiBackedRecord, DoiVersionedRepositoryClient,
734        DownloadNamedPublicFile, DraftFilePolicy, DraftFilePolicyKind,
735        DraftPublishingRepositoryClient, DraftResource, DraftState, DraftWorkflow,
736        ExistingFileConflictPolicy, ExistingFileConflictPolicyKind, ListResourceFiles, LookupByDoi,
737        MaybeAuthenticatedClient, MutablePublicationOutcome, NoCreateTarget, PublicationOutcome,
738        ReadPublicResource, RepositoryFile, RepositoryRecord, ResolveLatestPublicResource,
739        ResolveLatestPublicResourceByDoi, SearchPublicResources, SearchResultsLike,
740        UpdatePublication, UpdatePublicationRequest, UploadNameValidationError, UploadSourceKind,
741        UploadSpecLike,
742    };
743}
744
745#[cfg(test)]
746mod self_tests;