#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![deny(
clippy::all,
clippy::cargo,
clippy::pedantic,
clippy::expect_used,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::panic,
clippy::todo,
clippy::unimplemented,
clippy::unwrap_used
)]
#![allow(clippy::module_name_repetitions)]
use std::collections::BTreeSet;
use std::error::Error as StdError;
use std::fmt;
use std::future::Future;
use std::path::Path;
use std::time::Duration;
use url::Url;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct NoCreateTarget;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum UploadSourceKind {
Path,
Reader,
Bytes,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DraftFilePolicyKind {
ReplaceAll,
UpsertByFilename,
KeepExistingAndAdd,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ExistingFileConflictPolicyKind {
Error,
Skip,
Overwrite,
OverwriteKeepingHistory,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CreatePublicationRequest<Target, Metadata, Upload> {
pub target: Target,
pub metadata: Metadata,
pub uploads: Vec<Upload>,
}
impl<Target, Metadata, Upload> CreatePublicationRequest<Target, Metadata, Upload> {
#[must_use]
pub fn new(target: Target, metadata: Metadata, uploads: Vec<Upload>) -> Self {
Self {
target,
metadata,
uploads,
}
}
#[must_use]
pub fn with_upload(mut self, upload: Upload) -> Self {
self.uploads.push(upload);
self
}
#[must_use]
pub fn map_uploads<MappedUpload>(
self,
mut map: impl FnMut(Upload) -> MappedUpload,
) -> CreatePublicationRequest<Target, Metadata, MappedUpload> {
CreatePublicationRequest {
target: self.target,
metadata: self.metadata,
uploads: self.uploads.into_iter().map(&mut map).collect(),
}
}
}
impl<Metadata, Upload> CreatePublicationRequest<NoCreateTarget, Metadata, Upload> {
#[must_use]
pub fn untargeted(metadata: Metadata, uploads: Vec<Upload>) -> Self {
Self::new(NoCreateTarget, metadata, uploads)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UpdatePublicationRequest<ResourceId, Metadata, Policy, Upload> {
pub resource_id: ResourceId,
pub metadata: Metadata,
pub policy: Policy,
pub uploads: Vec<Upload>,
}
impl<ResourceId, Metadata, Policy, Upload>
UpdatePublicationRequest<ResourceId, Metadata, Policy, Upload>
{
#[must_use]
pub fn new(
resource_id: ResourceId,
metadata: Metadata,
policy: Policy,
uploads: Vec<Upload>,
) -> Self {
Self {
resource_id,
metadata,
policy,
uploads,
}
}
#[must_use]
pub fn with_upload(mut self, upload: Upload) -> Self {
self.uploads.push(upload);
self
}
#[must_use]
pub fn map_uploads<MappedUpload>(
self,
mut map: impl FnMut(Upload) -> MappedUpload,
) -> UpdatePublicationRequest<ResourceId, Metadata, Policy, MappedUpload> {
UpdatePublicationRequest {
resource_id: self.resource_id,
metadata: self.metadata,
policy: self.policy,
uploads: self.uploads.into_iter().map(&mut map).collect(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum UploadNameValidationError {
EmptyFilename,
DuplicateFilename {
filename: String,
},
}
impl fmt::Display for UploadNameValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyFilename => f.write_str("upload filename cannot be empty"),
Self::DuplicateFilename { filename } => {
write!(f, "duplicate upload filename: {filename}")
}
}
}
}
impl StdError for UploadNameValidationError {}
pub trait ClientContext {
type Endpoint;
type PollOptions;
type Error;
fn endpoint(&self) -> &Self::Endpoint;
fn poll_options(&self) -> &Self::PollOptions;
fn request_timeout(&self) -> Option<Duration>;
fn connect_timeout(&self) -> Option<Duration>;
}
pub trait MaybeAuthenticatedClient: ClientContext {
fn has_auth(&self) -> bool;
}
pub trait UploadSpecLike {
fn filename(&self) -> &str;
fn source_kind(&self) -> UploadSourceKind;
fn content_length(&self) -> Option<u64> {
None
}
fn content_type(&self) -> Option<&str> {
None
}
}
pub trait RepositoryFile {
type Id: Clone;
fn file_id(&self) -> Option<Self::Id>;
fn file_name(&self) -> &str;
fn size_bytes(&self) -> Option<u64>;
fn checksum(&self) -> Option<&str> {
None
}
fn download_url(&self) -> Option<&Url> {
None
}
}
pub trait RepositoryRecord {
type Id: Clone;
type File: RepositoryFile;
fn resource_id(&self) -> Option<Self::Id>;
fn title(&self) -> Option<&str>;
fn files(&self) -> &[Self::File];
}
pub trait DoiBackedRecord {
type Doi: Clone;
fn doi(&self) -> Option<Self::Doi>;
}
pub trait DraftResource {
type Id: Clone;
type File: RepositoryFile;
fn draft_id(&self) -> Self::Id;
fn files(&self) -> &[Self::File];
}
pub trait PublicationOutcome {
type PublicResource;
fn public_resource(&self) -> &Self::PublicResource;
fn created(&self) -> Option<bool> {
None
}
}
pub trait MutablePublicationOutcome: PublicationOutcome {
type MutableResource;
fn mutable_resource(&self) -> Option<&Self::MutableResource>;
}
pub trait DraftState {
fn is_published(&self) -> bool;
fn allows_metadata_updates(&self) -> bool;
}
pub trait ReadPublicResource: ClientContext {
type ResourceId;
type Resource;
fn get_public_resource(
&self,
id: &Self::ResourceId,
) -> impl Future<Output = Result<Self::Resource, Self::Error>>;
}
pub trait SearchPublicResources: ClientContext {
type Query;
type SearchResults;
fn search_public_resources(
&self,
query: &Self::Query,
) -> impl Future<Output = Result<Self::SearchResults, Self::Error>>;
}
pub trait SearchResultsLike {
type Item;
fn items(&self) -> &[Self::Item];
fn total_hits(&self) -> Option<u64> {
None
}
#[must_use]
fn page_len(&self) -> usize {
self.items().len()
}
#[must_use]
fn is_empty(&self) -> bool {
self.items().is_empty()
}
}
impl<T> SearchResultsLike for Vec<T> {
type Item = T;
fn items(&self) -> &[Self::Item] {
self.as_slice()
}
}
pub trait ListResourceFiles: ClientContext {
type ResourceId;
type File;
fn list_resource_files(
&self,
id: &Self::ResourceId,
) -> impl Future<Output = Result<Vec<Self::File>, Self::Error>>;
}
pub trait DownloadNamedPublicFile: ClientContext {
type ResourceId;
type Download;
fn download_named_public_file_to_path(
&self,
id: &Self::ResourceId,
name: &str,
path: &Path,
) -> impl Future<Output = Result<Self::Download, Self::Error>>;
}
pub trait CreatePublication: ClientContext {
type CreateTarget;
type Metadata;
type Upload;
type Output;
fn create_publication(
&self,
request: CreatePublicationRequest<Self::CreateTarget, Self::Metadata, Self::Upload>,
) -> impl Future<Output = Result<Self::Output, Self::Error>>;
}
pub trait UpdatePublication: ClientContext {
type ResourceId;
type Metadata;
type FilePolicy;
type Upload;
type Output;
fn update_publication(
&self,
request: UpdatePublicationRequest<
Self::ResourceId,
Self::Metadata,
Self::FilePolicy,
Self::Upload,
>,
) -> impl Future<Output = Result<Self::Output, Self::Error>>;
}
pub trait LookupByDoi: ClientContext {
type Doi;
type Resource;
fn get_public_resource_by_doi(
&self,
doi: &Self::Doi,
) -> impl Future<Output = Result<Self::Resource, Self::Error>>;
}
pub trait ResolveLatestPublicResource: ClientContext {
type ResourceId;
type Resource;
fn resolve_latest_public_resource(
&self,
id: &Self::ResourceId,
) -> impl Future<Output = Result<Self::Resource, Self::Error>>;
}
pub trait ResolveLatestPublicResourceByDoi: ClientContext {
type Doi;
type Resource;
fn resolve_latest_public_resource_by_doi(
&self,
doi: &Self::Doi,
) -> impl Future<Output = Result<Self::Resource, Self::Error>>;
}
pub trait DraftFilePolicy {
fn kind(&self) -> DraftFilePolicyKind;
}
pub trait ExistingFileConflictPolicy {
fn kind(&self) -> ExistingFileConflictPolicyKind;
}
pub trait DraftWorkflow: ClientContext {
type Draft: DraftResource;
type Metadata;
type Upload;
type FilePolicy;
type UploadResult;
type Published;
fn create_draft(
&self,
metadata: &Self::Metadata,
) -> impl Future<Output = Result<Self::Draft, Self::Error>>;
fn update_draft_metadata(
&self,
draft_id: &<Self::Draft as DraftResource>::Id,
metadata: &Self::Metadata,
) -> impl Future<Output = Result<Self::Draft, Self::Error>>;
fn reconcile_draft_files(
&self,
draft: &Self::Draft,
policy: Self::FilePolicy,
uploads: Vec<Self::Upload>,
) -> impl Future<Output = Result<Vec<Self::UploadResult>, Self::Error>>;
fn publish_draft(
&self,
draft_id: &<Self::Draft as DraftResource>::Id,
) -> impl Future<Output = Result<Self::Published, Self::Error>>;
}
pub trait CoreRepositoryClient:
ClientContext
+ CreatePublication
+ DownloadNamedPublicFile
+ ListResourceFiles
+ ReadPublicResource
+ SearchPublicResources
+ UpdatePublication
{
}
impl<T> CoreRepositoryClient for T where
T: ClientContext
+ CreatePublication
+ DownloadNamedPublicFile
+ ListResourceFiles
+ ReadPublicResource
+ SearchPublicResources
+ UpdatePublication
{
}
pub trait DoiVersionedRepositoryClient:
CoreRepositoryClient + LookupByDoi + ResolveLatestPublicResource + ResolveLatestPublicResourceByDoi
{
}
impl<T> DoiVersionedRepositoryClient for T where
T: CoreRepositoryClient
+ LookupByDoi
+ ResolveLatestPublicResource
+ ResolveLatestPublicResourceByDoi
{
}
pub trait DraftPublishingRepositoryClient: CoreRepositoryClient + DraftWorkflow {}
impl<T> DraftPublishingRepositoryClient for T where T: CoreRepositoryClient + DraftWorkflow {}
#[must_use]
pub fn find_file_by_name<'a, File>(
files: impl IntoIterator<Item = &'a File>,
name: &str,
) -> Option<&'a File>
where
File: RepositoryFile + ?Sized + 'a,
{
files.into_iter().find(|file| file.file_name() == name)
}
#[must_use]
pub fn has_file_named<'a, File>(files: impl IntoIterator<Item = &'a File>, name: &str) -> bool
where
File: RepositoryFile + ?Sized + 'a,
{
find_file_by_name(files, name).is_some()
}
#[must_use]
pub fn find_embedded_record_file_by_name<'a, Record>(
record: &'a Record,
name: &str,
) -> Option<&'a Record::File>
where
Record: RepositoryRecord + ?Sized,
{
find_file_by_name(record.files().iter(), name)
}
#[must_use]
pub fn find_embedded_draft_file_by_name<'a, Draft>(
draft: &'a Draft,
name: &str,
) -> Option<&'a Draft::File>
where
Draft: DraftResource + ?Sized,
{
find_file_by_name(draft.files().iter(), name)
}
pub fn collect_upload_filenames<'a, Upload>(
uploads: impl IntoIterator<Item = &'a Upload>,
) -> Result<BTreeSet<String>, UploadNameValidationError>
where
Upload: UploadSpecLike + ?Sized + 'a,
{
let mut filenames = BTreeSet::new();
for upload in uploads {
let filename = upload.filename();
if filename.is_empty() {
return Err(UploadNameValidationError::EmptyFilename);
}
if !filenames.insert(filename.to_owned()) {
return Err(UploadNameValidationError::DuplicateFilename {
filename: filename.to_owned(),
});
}
}
Ok(filenames)
}
pub fn validate_upload_filenames<'a, Upload>(
uploads: impl IntoIterator<Item = &'a Upload>,
) -> Result<(), UploadNameValidationError>
where
Upload: UploadSpecLike + ?Sized + 'a,
{
collect_upload_filenames(uploads).map(|_| ())
}
#[must_use]
pub fn has_upload_filename<'a, Upload>(
uploads: impl IntoIterator<Item = &'a Upload>,
filename: &str,
) -> bool
where
Upload: UploadSpecLike + ?Sized + 'a,
{
uploads
.into_iter()
.any(|upload| upload.filename() == filename)
}
pub mod prelude {
pub use crate::{
collect_upload_filenames, find_embedded_draft_file_by_name,
find_embedded_record_file_by_name, find_file_by_name, has_file_named, has_upload_filename,
validate_upload_filenames, ClientContext, CoreRepositoryClient, CreatePublication,
CreatePublicationRequest, DoiBackedRecord, DoiVersionedRepositoryClient,
DownloadNamedPublicFile, DraftFilePolicy, DraftFilePolicyKind,
DraftPublishingRepositoryClient, DraftResource, DraftState, DraftWorkflow,
ExistingFileConflictPolicy, ExistingFileConflictPolicyKind, ListResourceFiles, LookupByDoi,
MaybeAuthenticatedClient, MutablePublicationOutcome, NoCreateTarget, PublicationOutcome,
ReadPublicResource, RepositoryFile, RepositoryRecord, ResolveLatestPublicResource,
ResolveLatestPublicResourceByDoi, SearchPublicResources, SearchResultsLike,
UpdatePublication, UpdatePublicationRequest, UploadNameValidationError, UploadSourceKind,
UploadSpecLike,
};
}
#[cfg(test)]
mod self_tests;