Adding initial code for Suzurubooru client
This commit is contained in:
commit
8d57f5d9de
11 changed files with 4982 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
15
.pre-commit-config.yaml
Normal file
15
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v3.2.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-added-large-files
|
||||||
|
- repo: https://github.com/FeryET/pre-commit-rust
|
||||||
|
rev: v1.1.0
|
||||||
|
hooks:
|
||||||
|
- id: fmt
|
||||||
|
- id: cargo-check
|
||||||
1892
Cargo.lock
generated
Normal file
1892
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "szurubooru-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
base64 = "0.22.1"
|
||||||
|
bytes = "1.6.1"
|
||||||
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
|
derive_builder = "0.20.0"
|
||||||
|
hex = "0.4.3"
|
||||||
|
reqwest = { version = "0.12.5", features = ["json", "multipart"] }
|
||||||
|
serde = { version = "1.0.204", features = ["derive"] }
|
||||||
|
serde_json = "1.0.120"
|
||||||
|
sha1 = "0.10.6"
|
||||||
|
strum = { version = "0.26.3", features = ["derive", "strum_macros"] }
|
||||||
|
strum_macros = "0.26.4"
|
||||||
|
thiserror = "1.0.63"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
url = "2.5.2"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
mockito = "1.4.0"
|
||||||
|
tokio = { version = "1.39.2", features = ["full"] }
|
||||||
0
docker-compose.yml
Normal file
0
docker-compose.yml
Normal file
1220
src/client.rs
Normal file
1220
src/client.rs
Normal file
File diff suppressed because it is too large
Load diff
199
src/errors.rs
Normal file
199
src/errors.rs
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
//! Types that represent the various error states that can occur when interacting with
|
||||||
|
//! Szurubooru
|
||||||
|
|
||||||
|
use crate::models::SzuruEither;
|
||||||
|
use base64::EncodeSliceError;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
use url::ParseError as UParseError;
|
||||||
|
|
||||||
|
/// Trait to support conversion into a [SzurubooruResult]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub trait IntoClientResult<T> {
|
||||||
|
/// Convert `self` into a SzurubooruResult
|
||||||
|
fn into_result(self) -> SzurubooruResult<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
/// Type that represents the various error states that can occur when interacting with
|
||||||
|
/// Szurubooru
|
||||||
|
pub enum SzurubooruClientError {
|
||||||
|
/// Error occurred when trying to Bas64 encode the `username:token` string
|
||||||
|
#[error("Error encoding authentication token: {0}")]
|
||||||
|
Base64EncodingError(#[source] EncodeSliceError),
|
||||||
|
/// Error parsing the given host URL
|
||||||
|
#[error("Error parsing URL {url}: {source}")]
|
||||||
|
UrlParseError {
|
||||||
|
/// The resulting error
|
||||||
|
source: UParseError,
|
||||||
|
/// The URL in question
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
|
/// Error occurred building the request before it's sent to the server
|
||||||
|
#[error("Error building request {0}")]
|
||||||
|
RequestBuilderError(#[source] reqwest::Error),
|
||||||
|
/// Error occurred pas part of the request to the server
|
||||||
|
#[error("Request error {0}")]
|
||||||
|
RequestError(#[source] reqwest::Error),
|
||||||
|
/// Error parsing the JSON response from the server
|
||||||
|
#[error("Response Parsing error: {0}: {1}")]
|
||||||
|
ResponseParsingError(
|
||||||
|
/// The JSON parsing error
|
||||||
|
#[source]
|
||||||
|
serde_json::Error,
|
||||||
|
/// The string we attempted to parse
|
||||||
|
String,
|
||||||
|
),
|
||||||
|
/// Error serializing an object as JSON
|
||||||
|
#[error("JSON Serialization error: {0}")]
|
||||||
|
JSONSerializationError(#[source] serde_json::Error),
|
||||||
|
/// Error occurred when reading a file
|
||||||
|
#[error("IO Error: {0}")]
|
||||||
|
IOError(#[source] std::io::Error),
|
||||||
|
/// Error returned by the Szurubooru server
|
||||||
|
#[error("Error returned from Szurubooru host: {0:?}")]
|
||||||
|
SzurubooruServerError(SzurubooruServerError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SzurubooruServerError> for SzurubooruClientError {
|
||||||
|
fn from(value: SzurubooruServerError) -> Self {
|
||||||
|
SzurubooruClientError::SzurubooruServerError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type used to represent success or a failure of some kind
|
||||||
|
pub type SzurubooruResult<T> = Result<T, SzurubooruClientError>;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
impl<T> IntoClientResult<T> for SzuruEither<T, SzurubooruServerError> {
|
||||||
|
fn into_result(self) -> SzurubooruResult<T> {
|
||||||
|
match self {
|
||||||
|
SzuruEither::Left(v) => Ok(v),
|
||||||
|
SzuruEither::Right(e) => Err(SzurubooruClientError::SzurubooruServerError(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
/// An error type returned by the server
|
||||||
|
pub enum SzurubooruServerErrorType {
|
||||||
|
/// Missing required file
|
||||||
|
MissingRequiredFileError,
|
||||||
|
/// Missing required parameter
|
||||||
|
MissingRequiredParameterError,
|
||||||
|
/// Invalid parameter
|
||||||
|
InvalidParameterError,
|
||||||
|
/// Integrity error
|
||||||
|
IntegrityError,
|
||||||
|
/// Search error
|
||||||
|
SearchError,
|
||||||
|
/// Invalid authentication
|
||||||
|
AuthError,
|
||||||
|
/// Post with the given ID not found
|
||||||
|
PostNotFoundError,
|
||||||
|
/// Post is already featured
|
||||||
|
PostAlreadyFeaturedError,
|
||||||
|
/// Post is already uploaded
|
||||||
|
PostAlreadyUploadedError,
|
||||||
|
/// Invalid post ID
|
||||||
|
InvalidPostIdError,
|
||||||
|
/// Invalid Post Safety
|
||||||
|
InvalidPostSafetyError,
|
||||||
|
/// Invalid post source
|
||||||
|
InvalidPostSourceError,
|
||||||
|
/// Invalid post content
|
||||||
|
InvalidPostContentError,
|
||||||
|
/// Invalid post relation
|
||||||
|
InvalidPostRelationError,
|
||||||
|
/// Invalid post note
|
||||||
|
InvalidPostNoteError,
|
||||||
|
/// Invalid post flag
|
||||||
|
InvalidPostFlagError,
|
||||||
|
/// Invalid favorite target
|
||||||
|
InvalidFavoriteTargetError,
|
||||||
|
/// Invalid comment ID
|
||||||
|
InvalidCommentIdError,
|
||||||
|
/// Comment not found
|
||||||
|
CommentNotFoundError,
|
||||||
|
/// Empty comment text
|
||||||
|
EmptyCommentTextError,
|
||||||
|
/// Invalid score target
|
||||||
|
InvalidScoreTargetError,
|
||||||
|
/// Invalid score value
|
||||||
|
InvalidScoreValueError,
|
||||||
|
/// Tag category not found
|
||||||
|
TagCategoryNotFoundError,
|
||||||
|
/// Tag category already exists
|
||||||
|
TagCategoryAlreadyExistsError,
|
||||||
|
/// Tag category is in use
|
||||||
|
TagCategoryIsInUseError,
|
||||||
|
/// Invalid tag category name
|
||||||
|
InvalidTagCategoryNameError,
|
||||||
|
/// Invalid tag category color
|
||||||
|
InvalidTagCategoryColorError,
|
||||||
|
/// Tag not found
|
||||||
|
TagNotFoundError,
|
||||||
|
/// Tag already exists
|
||||||
|
TagAlreadyExistsError,
|
||||||
|
/// Tag is in use
|
||||||
|
TagIsInUseError,
|
||||||
|
/// Invalid tag name
|
||||||
|
InvalidTagNameError,
|
||||||
|
/// Invalid tag relation
|
||||||
|
InvalidTagRelationError,
|
||||||
|
/// Invalid tag category
|
||||||
|
InvalidTagCategoryError,
|
||||||
|
/// Invalid tag description
|
||||||
|
InvalidTagDescriptionError,
|
||||||
|
/// User not found
|
||||||
|
UserNotFoundError,
|
||||||
|
/// User already exists
|
||||||
|
UserAlreadyExistsError,
|
||||||
|
/// Invalid user name
|
||||||
|
InvalidUserNameError,
|
||||||
|
/// Invalid email
|
||||||
|
InvalidEmailError,
|
||||||
|
/// Invalid password
|
||||||
|
InvalidPasswordError,
|
||||||
|
/// Invalid rank
|
||||||
|
InvalidRankError,
|
||||||
|
/// Invalid avatar
|
||||||
|
InvalidAvatarError,
|
||||||
|
/// Processing error
|
||||||
|
ProcessingError,
|
||||||
|
/// Validation error
|
||||||
|
ValidationError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
/// Type describing an error returned from Szurubooru
|
||||||
|
pub struct SzurubooruServerError {
|
||||||
|
/// The name (or type) of error
|
||||||
|
pub name: SzurubooruServerErrorType,
|
||||||
|
/// Title of the error
|
||||||
|
pub title: String,
|
||||||
|
/// More of a description of the error
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_server_error() {
|
||||||
|
let json_response = r#"{
|
||||||
|
"name": "ValidationError",
|
||||||
|
"title": "Validation Error",
|
||||||
|
"description": "Some sort of validation error"
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let sse = serde_json::from_str::<SzurubooruServerError>(json_response)
|
||||||
|
.expect("Failed to parse the JSON response");
|
||||||
|
|
||||||
|
assert_eq!(sse.name, SzurubooruServerErrorType::ValidationError);
|
||||||
|
assert_eq!(sse.title, "Validation Error");
|
||||||
|
assert_eq!(sse.description, "Some sort of validation error");
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/lib.rs
Normal file
16
src/lib.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
//! The core library for SzurubooruClient
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
#![warn(rustdoc::missing_crate_level_docs)]
|
||||||
|
|
||||||
|
/// Core client module
|
||||||
|
pub mod client;
|
||||||
|
pub use client::SzurubooruClient;
|
||||||
|
pub use client::SzurubooruRequest;
|
||||||
|
|
||||||
|
pub mod errors;
|
||||||
|
pub use errors::SzurubooruResult;
|
||||||
|
pub mod models;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
pub mod tokens;
|
||||||
925
src/models.rs
Normal file
925
src/models.rs
Normal file
|
|
@ -0,0 +1,925 @@
|
||||||
|
//! Types that represent the various API objects returned by Szurubooru. Many of the `Resource`
|
||||||
|
//! objects have all of their fields as [Option] types because the Server API supports field
|
||||||
|
//! selection.
|
||||||
|
//!
|
||||||
|
//! See [here](https://github.com/rr-/szurubooru/blob/master/doc/API.md#field-selecting) for
|
||||||
|
//! more information.
|
||||||
|
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use derive_builder::Builder;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum_macros::AsRefStr;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
/// Enum used to represent something that's either `Left` or `Right`
|
||||||
|
pub enum SzuruEither<L, R> {
|
||||||
|
/// Enum variant `Left`
|
||||||
|
Left(L),
|
||||||
|
/// Enum variant `Right`
|
||||||
|
Right(R),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
/// A result of search operation that doesn't involve paging
|
||||||
|
pub struct UnpagedSearchResult<T> {
|
||||||
|
/// The total list of results
|
||||||
|
pub results: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
/// A result of search operation that involves paging
|
||||||
|
///
|
||||||
|
/// Use (offset)[crate::SzurubooruRequest::offset] and (limit)[crate::SzurubooruRequest::limit]
|
||||||
|
/// to fetch the next page
|
||||||
|
pub struct PagedSearchResult<T> {
|
||||||
|
/// The original query for the request
|
||||||
|
pub query: String,
|
||||||
|
/// The number of [T] to skip forward
|
||||||
|
pub offset: u32,
|
||||||
|
/// The maximum number of [T] to return
|
||||||
|
pub limit: u32,
|
||||||
|
/// The total number of [T] that match the [query](PagedSearchResult::query)
|
||||||
|
pub total: u32,
|
||||||
|
/// The results themselves
|
||||||
|
pub results: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
/// A [tag resource](TagResource) stripped down to `names`, `category` and `usages` fields.
|
||||||
|
pub struct MicroTagResource {
|
||||||
|
/// The tag names and aliases
|
||||||
|
pub names: Vec<String>,
|
||||||
|
/// The category this tag belongs to
|
||||||
|
pub category: String,
|
||||||
|
/// The number of times this tag has been used
|
||||||
|
pub usages: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
/// To prevent problems with concurrent resource modification, Szurubooru implements optimistic
|
||||||
|
/// locks using resource versions. Each modifiable resource has its version returned to the client
|
||||||
|
/// with `GET` requests. At the same time, each `PUT` and `DELETE` request sent by the client
|
||||||
|
/// must present the same version field to the server with value as it was given in `GET`.
|
||||||
|
///
|
||||||
|
/// For example, given `GET /post/1`, the server responds like this:
|
||||||
|
///
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// ...,
|
||||||
|
/// "version": 2
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This means the client must then send `{"version": 2}` back too. If the client fails to do so,
|
||||||
|
/// the server will reject the request notifying about missing parameter. If someone has edited the
|
||||||
|
/// post in the meantime, the server will reject the request as well, in which case the client is
|
||||||
|
/// encouraged to notify the user about the situation.
|
||||||
|
pub struct ResourceVersion {
|
||||||
|
/// The version itself
|
||||||
|
pub version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A single tag. Tags are used to let users search for posts.
|
||||||
|
pub struct TagResource {
|
||||||
|
/// resource version. See [versioning](ResourceVersion)
|
||||||
|
pub version: u32,
|
||||||
|
/// a list of tag names (aliases). Tagging a post with any name will automatically assign
|
||||||
|
/// the first name from this list.
|
||||||
|
pub names: Option<Vec<String>>,
|
||||||
|
/// the name of the category the given tag belongs to
|
||||||
|
pub category: Option<String>,
|
||||||
|
/// a list of implied tags, serialized as micro tag resource. Implied tags are automatically
|
||||||
|
/// appended by the web client on usage.
|
||||||
|
pub implications: Option<Vec<MicroTagResource>>,
|
||||||
|
/// a list of suggested tags, serialized as micro tag resource. Suggested tags are shown to
|
||||||
|
/// the user by the web client on usage
|
||||||
|
pub suggestions: Option<Vec<MicroTagResource>>,
|
||||||
|
/// time the tag was created
|
||||||
|
pub creation_time: Option<NaiveDateTime>,
|
||||||
|
/// time the tag was edited
|
||||||
|
pub last_edit_time: Option<NaiveDateTime>,
|
||||||
|
/// the number of posts the tag was used in
|
||||||
|
pub usages: Option<u32>,
|
||||||
|
/// the tag description (instructions how to use, history etc.) The client should render
|
||||||
|
/// is as Markdown
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates or updates a tag using specified parameters. Names, suggestions and implications must
|
||||||
|
/// match `tag_name_regex` from server's configuration. Category must exist and is the same as name
|
||||||
|
/// field within <tag-category> resource. Suggestions and implications are optional. If specified
|
||||||
|
/// implied tags or suggested tags do not exist yet, they will be automatically created. Tags
|
||||||
|
/// created automatically have no implications, no suggestions, one name and their category is set
|
||||||
|
/// to the first tag category found. If there are no tag categories established yet, an error
|
||||||
|
/// will be thrown.
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use szurubooru_client::models::CreateUpdateTagBuilder;
|
||||||
|
/// let cu_tag = CreateUpdateTagBuilder::default()
|
||||||
|
/// .version(1)
|
||||||
|
/// .names(vec!["foo_tag".to_string()])
|
||||||
|
/// .build()
|
||||||
|
/// .expect("A new tag");
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
|
||||||
|
#[builder(setter(strip_option))]
|
||||||
|
pub struct CreateUpdateTag {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// resource version. See [versioning](ResourceVersion)
|
||||||
|
pub version: Option<u32>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Tag names and aliases, must match `tag_name_regex` from the server's configuration
|
||||||
|
pub names: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Category that this tag belongs to. Must already exist
|
||||||
|
pub category: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// The tag description in Markdown format
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Tags that should be implied when this tag is used
|
||||||
|
pub implications: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Tags that should be suggested when this tag is used
|
||||||
|
pub suggestions: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
/// A single tag category. The primary purpose of tag categories is to distinguish certain tag
|
||||||
|
/// types (such as characters, media type etc.), which improves user experience.
|
||||||
|
pub struct TagCategoryResource {
|
||||||
|
/// resource version. See [versioning](ResourceVersion)
|
||||||
|
pub version: u32,
|
||||||
|
/// The name of the tag category
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// The display color of the tag category
|
||||||
|
pub color: Option<String>,
|
||||||
|
/// How many tags is the given category used with
|
||||||
|
pub usages: Option<u32>,
|
||||||
|
/// The order in which tags with this category are displayed, ascending
|
||||||
|
pub order: Option<String>,
|
||||||
|
/// Whether the tag category is the default one
|
||||||
|
pub default: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
|
||||||
|
#[builder(setter(into))]
|
||||||
|
/// Used for creating or updating a Tag Category
|
||||||
|
pub struct CreateUpdateTagCategory {
|
||||||
|
/// The name of the category to create
|
||||||
|
pub name: String,
|
||||||
|
/// The display color to use for the category
|
||||||
|
pub color: String,
|
||||||
|
/// The order in which tags with this category are displayed, ascending
|
||||||
|
pub order: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[builder(setter(into))]
|
||||||
|
/// Removes source tag and merges all of its usages, suggestions and implications to the target tag.
|
||||||
|
/// Other tag properties such as category and aliases do not get transferred and are discarded.
|
||||||
|
pub struct MergeTags {
|
||||||
|
/// Version of the tag to remove
|
||||||
|
pub remove_version: u32,
|
||||||
|
/// The name of the tag to remove
|
||||||
|
pub remove: String,
|
||||||
|
/// The version of the tag to merge TO
|
||||||
|
pub merge_to_version: u32,
|
||||||
|
/// The name of the tag to merge TO
|
||||||
|
pub merge_to: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
/// Lists siblings of given tag, e.g. tags that were used in the same posts as the given tag
|
||||||
|
pub struct TagSibling {
|
||||||
|
/// The related tag
|
||||||
|
pub tag: TagResource,
|
||||||
|
/// How many times a given tag appears with the given tag
|
||||||
|
pub occurrences: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr)]
|
||||||
|
/// The type of post
|
||||||
|
pub enum PostType {
|
||||||
|
/// Image post
|
||||||
|
Image,
|
||||||
|
/// Animated post
|
||||||
|
Animation,
|
||||||
|
/// Alias of [Animation](PostType::Animation)
|
||||||
|
Animated,
|
||||||
|
/// Alias of [Animation](PostType::Animation)
|
||||||
|
Anim,
|
||||||
|
/// Flash animation
|
||||||
|
Flash,
|
||||||
|
/// Alias of [Flash](PostType::Flash)
|
||||||
|
Swf,
|
||||||
|
/// Video post of some type. See the mime type for more information
|
||||||
|
Video,
|
||||||
|
/// Webm container type
|
||||||
|
Webm,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr)]
|
||||||
|
/// How SFW/NSFW the post is
|
||||||
|
pub enum PostSafety {
|
||||||
|
/// Post is SFW
|
||||||
|
Safe,
|
||||||
|
/// Post is possibly NSFW
|
||||||
|
Sketchy,
|
||||||
|
/// Alias of (Sketchy)[PostSafety::Sketchy]
|
||||||
|
Questionable,
|
||||||
|
/// Post is NSFW
|
||||||
|
Unsafe,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A post resource stripped down to `id` and `thumbnailUrl` fields.
|
||||||
|
pub struct MicroPostResource {
|
||||||
|
/// The ID of the post
|
||||||
|
pub id: u32,
|
||||||
|
/// The thumbnail URL of the post
|
||||||
|
pub thumbnail_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(crate) struct PostId {
|
||||||
|
pub id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A post resource
|
||||||
|
pub struct PostResource {
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// The post identifier
|
||||||
|
pub id: Option<u32>,
|
||||||
|
/// Time the post was created
|
||||||
|
pub creation_time: Option<NaiveDateTime>,
|
||||||
|
/// Time the post was edited
|
||||||
|
pub last_edit_time: Option<NaiveDateTime>,
|
||||||
|
/// Whether the post is safe for work
|
||||||
|
pub safety: Option<PostSafety>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
/// The type of the post
|
||||||
|
pub post_type: Option<PostType>,
|
||||||
|
/// Where the post was grabbed form, supplied by the user
|
||||||
|
pub source: Option<String>,
|
||||||
|
/// The SHA1 file checksum. Used in snapshots to signify changes of the post content
|
||||||
|
pub checksum: Option<String>,
|
||||||
|
#[serde(rename = "checksumMD5")]
|
||||||
|
/// The MD5 file checksum
|
||||||
|
pub checksum_md5: Option<String>,
|
||||||
|
/// The original width of the post content.
|
||||||
|
pub canvas_width: Option<u32>,
|
||||||
|
/// The original height of the post content.
|
||||||
|
pub canvas_height: Option<u32>,
|
||||||
|
/// Where the post content is located
|
||||||
|
pub content_url: Option<String>,
|
||||||
|
/// Where the post thumbnail is located
|
||||||
|
pub thumbnail_url: Option<String>,
|
||||||
|
/// Various flags such as whether the post is looped
|
||||||
|
pub flags: Option<Vec<String>>,
|
||||||
|
/// List of tags the post is tagged with
|
||||||
|
pub tags: Option<Vec<MicroTagResource>>,
|
||||||
|
/// A list of related posts.
|
||||||
|
pub relations: Option<Vec<MicroPostResource>>,
|
||||||
|
/// A list of post annotations
|
||||||
|
pub notes: Option<Vec<NoteResource>>,
|
||||||
|
/// Who created the post
|
||||||
|
pub user: Option<MicroUserResource>,
|
||||||
|
/// The collective score (+1/-1 rating) of the given post
|
||||||
|
pub score: Option<i32>,
|
||||||
|
/// Whether the authenticated user has given post in their favorites
|
||||||
|
pub own_favorite: Option<bool>,
|
||||||
|
/// How many tags the post is tagged with
|
||||||
|
pub tag_count: Option<u32>,
|
||||||
|
/// How many users have the post in their favorites
|
||||||
|
pub favorite_count: Option<u32>,
|
||||||
|
/// How many comments are filed under that post
|
||||||
|
pub comment_count: Option<u32>,
|
||||||
|
/// How many notes the post has
|
||||||
|
pub note_count: Option<u32>,
|
||||||
|
/// How many times has the post been featured
|
||||||
|
pub feature_count: Option<u32>,
|
||||||
|
/// How many posts are related to this post
|
||||||
|
pub relation_count: Option<u32>,
|
||||||
|
/// The last time the post was featured
|
||||||
|
pub last_feature_time: Option<NaiveDateTime>,
|
||||||
|
/// List of users who have favorited this post
|
||||||
|
pub favorited_by: Option<Vec<MicroUserResource>>,
|
||||||
|
/// Whether the post uses custom thumbnail
|
||||||
|
pub has_custom_thumbnail: Option<bool>,
|
||||||
|
/// Subsidiary to [type](PostResource::post_type), used to tell exact content format;
|
||||||
|
/// useful for `<video>` tags for instance
|
||||||
|
pub mime_type: Option<String>,
|
||||||
|
/// All the comments on the post
|
||||||
|
pub comment: Option<Vec<CommentResource>>,
|
||||||
|
/// The pools in which the post is a member
|
||||||
|
pub pools: Option<Vec<PoolResource>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
|
||||||
|
#[builder(setter(into, strip_option))]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A `struct` used to create or update a post. For updating purposes
|
||||||
|
/// the [version](CreateUpdatePost::version) field is required
|
||||||
|
pub struct CreateUpdatePost {
|
||||||
|
/// Tags to use for this post. If specified tags do not exist yet, they will be automatically
|
||||||
|
/// created. Tags created automatically have no implications, no suggestions, one name and
|
||||||
|
/// their category is set to the first tag category found
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
/// Required field, represents the SFW/NSFW state of a post
|
||||||
|
pub safety: PostSafety,
|
||||||
|
/// The origin of the post's content
|
||||||
|
pub source: Option<String>,
|
||||||
|
/// The IDs of related posts
|
||||||
|
pub relations: Option<Vec<u32>>,
|
||||||
|
/// Notes to be displayed on the post
|
||||||
|
pub notes: Option<Vec<NoteResource>>,
|
||||||
|
/// Flags relevant to the post. If omitted they will be auto-detected
|
||||||
|
pub flags: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// The URL to download the content from
|
||||||
|
pub content_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// The token returned from
|
||||||
|
/// [upload_temporary_file](crate::SzurubooruRequest::upload_temporary_file)
|
||||||
|
pub content_token: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
pub version: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A token representing a temporary file upload
|
||||||
|
pub struct TemporaryFileUpload {
|
||||||
|
/// Temporary upload token
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
|
||||||
|
#[builder(setter(into))]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// Removes source post and merges all of its tags, relations, scores, favorites and comments to
|
||||||
|
/// the target post. If replaceContent is set to true, content of the target post is replaced using
|
||||||
|
/// the content of the source post; otherwise it remains unchanged. Source post properties such as
|
||||||
|
/// its safety, source, whether to loop the video and other scalar values do not get transferred
|
||||||
|
/// and are discarded.
|
||||||
|
pub struct MergePost {
|
||||||
|
/// The version of the post to remove
|
||||||
|
pub remove_version: u32,
|
||||||
|
/// The ID of the post to remove
|
||||||
|
pub remove: u32,
|
||||||
|
/// The version of the post to merge TO
|
||||||
|
pub merge_to_version: u32,
|
||||||
|
/// The post ID of the post to merge TO
|
||||||
|
pub merge_to: u32,
|
||||||
|
/// Whether to replace the content
|
||||||
|
pub replace_content: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct RateResource {
|
||||||
|
pub score: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A text annotation rendered on top of the post
|
||||||
|
pub struct NoteResource {
|
||||||
|
/// Where to draw the annotation. Each point must have coordinates within 0 to 1.
|
||||||
|
/// For example, `[[0,0],[0,1],[1,1],[1,0]]` will draw the annotation on the whole post,
|
||||||
|
/// whereas `[[0,0],[0,0.5],[0.5,0.5],[0.5,0]]` will draw it inside the post's upper left
|
||||||
|
/// quarter
|
||||||
|
pub polygon: Vec<Vec<u8>>,
|
||||||
|
/// The annotation text, in Markdown format
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// The Rank of a given User
|
||||||
|
pub enum UserRank {
|
||||||
|
/// Restricted, limited user
|
||||||
|
Restricted,
|
||||||
|
/// Regular user
|
||||||
|
Regular,
|
||||||
|
/// Power user
|
||||||
|
Power,
|
||||||
|
/// Moderator user
|
||||||
|
Moderator,
|
||||||
|
/// All-powerful Administrator
|
||||||
|
Administrator,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// The kind of User Avatar
|
||||||
|
pub enum UserAvatarStyle {
|
||||||
|
/// Automatically-generated Gravatar
|
||||||
|
Gravatar,
|
||||||
|
/// Manually updated avatar
|
||||||
|
Manual,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A single user
|
||||||
|
pub struct UserResource {
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// The user's username
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// The user email. It is available only if the request is authenticated by the same user,
|
||||||
|
/// or the authenticated user can change the email. If it's unavailable, the server returns
|
||||||
|
/// `false`. If the user hasn't specified an email, the server returns [None](Option::None)
|
||||||
|
pub email: Option<SzuruEither<String, bool>>,
|
||||||
|
/// The user rank, which effectively affects their privileges
|
||||||
|
pub rank: Option<UserRank>,
|
||||||
|
#[serde(rename = "last-login-time")]
|
||||||
|
/// The last login time
|
||||||
|
pub last_login_time: Option<NaiveDateTime>,
|
||||||
|
#[serde(rename = "creation-time")]
|
||||||
|
/// The user registration time
|
||||||
|
pub creation_time: Option<NaiveDateTime>,
|
||||||
|
/// How to render the user avatar
|
||||||
|
pub avatar_style: Option<UserAvatarStyle>,
|
||||||
|
/// The URL to the avatar
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
|
/// Number of comments
|
||||||
|
#[serde(rename = "comment-count")]
|
||||||
|
pub comment_count: Option<u32>,
|
||||||
|
/// Number of uploaded posts
|
||||||
|
#[serde(rename = "uploaded-post-count")]
|
||||||
|
pub uploaded_post_count: Option<u32>,
|
||||||
|
/// Number of liked posts. It is available only if the request is authenticated by the same
|
||||||
|
/// user. If it's unavailable, the server returns `false`
|
||||||
|
#[serde(rename = "liked-post-count")]
|
||||||
|
pub liked_post_count: Option<SzuruEither<u32, bool>>,
|
||||||
|
/// Number of disliked posts. It is available only if the request is authenticated by the same
|
||||||
|
/// user. If it's unavailable, the server returns `false`.
|
||||||
|
#[serde(rename = "disliked-post-count")]
|
||||||
|
pub disliked_post_count: Option<SzuruEither<u32, bool>>,
|
||||||
|
/// Number of favorited posts
|
||||||
|
#[serde(rename = "favorite-post-count")]
|
||||||
|
pub favorite_post_count: Option<SzuruEither<u32, bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
|
||||||
|
#[builder(setter(into, strip_option))]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// `struct` used to create or update a user resource. The version field is only used when
|
||||||
|
/// updating an existing resource
|
||||||
|
pub struct CreateUpdateUser {
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// The username
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// The user's password
|
||||||
|
pub password: Option<String>,
|
||||||
|
/// The user's desired rank, if not given will default to `default_rank` in the server's
|
||||||
|
/// configuration
|
||||||
|
pub rank: Option<UserRank>,
|
||||||
|
/// The user avatar style, Gravatar or Manual
|
||||||
|
pub avatar_style: Option<UserAvatarStyle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A user resource stripped down to `name` and `avatarUrl` fields
|
||||||
|
pub struct MicroUserResource {
|
||||||
|
/// The username
|
||||||
|
pub name: String,
|
||||||
|
/// The user's avatar URL
|
||||||
|
pub avatar_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
/// A single user token
|
||||||
|
pub struct UserAuthTokenResource {
|
||||||
|
/// A micro user resource
|
||||||
|
pub user: Option<MicroUserResource>,
|
||||||
|
/// The token that can be used to authenticate the user.
|
||||||
|
pub token: Option<String>,
|
||||||
|
/// A note that describes the token
|
||||||
|
pub note: Option<String>,
|
||||||
|
/// Whether the token is still valid for authentication
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
/// Time when the token expires
|
||||||
|
pub expiration_time: Option<NaiveDateTime>,
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// time the user token was created
|
||||||
|
pub creation_time: Option<NaiveDateTime>,
|
||||||
|
/// time the user token was edited
|
||||||
|
pub last_edit_time: Option<NaiveDateTime>,
|
||||||
|
/// the last time this token was used
|
||||||
|
pub last_usage_time: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
|
||||||
|
#[builder(setter(into, strip_option))]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
/// `struct` to create or update a UserAuthToken. `version` is only required when updating an
|
||||||
|
/// existing resource
|
||||||
|
pub struct CreateUpdateUserAuthToken {
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// Whether the token is still valid for authentication
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
/// A note that describes the token
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub note: Option<String>,
|
||||||
|
/// Time when the token expires
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub expiration_time: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct PasswordResetToken {
|
||||||
|
/// The password token received via email
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// Type that represents a new temporary password
|
||||||
|
pub struct TemporaryPassword {
|
||||||
|
/// The new temporary password generated once [PasswordResetToken] has been sent to the server
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// Simple server configuration
|
||||||
|
pub struct GlobalInfoConfig {
|
||||||
|
/// Regular expression that the username must match
|
||||||
|
pub user_name_regex: String,
|
||||||
|
/// Regular expression that the password must match
|
||||||
|
pub password_regex: String,
|
||||||
|
/// Regular expression that tag names must match
|
||||||
|
pub tag_name_regex: String,
|
||||||
|
/// Regular expression that tag category names must match
|
||||||
|
pub tag_category_name_regex: String,
|
||||||
|
/// Default user rank upon signup
|
||||||
|
pub default_user_rank: String,
|
||||||
|
/// Available privileges enabled for this server
|
||||||
|
pub privileges: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// Simple server statistics
|
||||||
|
pub struct GlobalInfo {
|
||||||
|
/// The total number of posts
|
||||||
|
pub post_count: u32,
|
||||||
|
/// Total disk usage
|
||||||
|
pub disk_usage: u32,
|
||||||
|
/// The current featured post
|
||||||
|
pub featured_post: Option<u32>,
|
||||||
|
/// The time the current featured post was featured
|
||||||
|
pub featuring_time: Option<NaiveDateTime>,
|
||||||
|
/// The user who uploaded the featured post
|
||||||
|
pub featuring_user: Option<u32>,
|
||||||
|
/// The current server time
|
||||||
|
pub server_time: NaiveDateTime,
|
||||||
|
/// The configuration for this server
|
||||||
|
pub config: GlobalInfoConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A single pool category. The primary purpose of pool categories is to distinguish certain pool
|
||||||
|
/// types (such as series, relations etc.), which improves user experience.
|
||||||
|
pub struct PoolCategoryResource {
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// The category name
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// The category color
|
||||||
|
pub color: Option<String>,
|
||||||
|
/// How many pools is the given category used with
|
||||||
|
pub usages: Option<u32>,
|
||||||
|
/// Whether the pool category is the default one
|
||||||
|
pub default: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
|
||||||
|
#[builder(setter(strip_option))]
|
||||||
|
/// `struct` used for creating or updating a pool category. This type uses a Builder pattern like
|
||||||
|
/// so:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use szurubooru_client::models::CreateUpdatePoolCategoryBuilder;
|
||||||
|
/// // Updating an existing pool category
|
||||||
|
/// let cu_pool_cat = CreateUpdatePoolCategoryBuilder::default()
|
||||||
|
/// .version(1)
|
||||||
|
/// .name("new_name".to_string())
|
||||||
|
/// .build()
|
||||||
|
/// .unwrap();
|
||||||
|
/// ```
|
||||||
|
pub struct CreateUpdatePoolCategory {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Category version (used for updating)
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// Category name
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// Category color
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// Type that represents a Pool resource
|
||||||
|
pub struct PoolResource {
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// The pool identifier
|
||||||
|
pub id: Option<u32>,
|
||||||
|
/// A list of pool names (aliases)
|
||||||
|
pub names: Option<Vec<String>>,
|
||||||
|
/// The name of the category the given pool belongs to
|
||||||
|
pub category: Option<String>,
|
||||||
|
/// An ordered list of posts. Posts are ordered by insertion by default
|
||||||
|
pub posts: Option<Vec<MicroPostResource>>,
|
||||||
|
/// Time the pool was created
|
||||||
|
pub creation_time: Option<NaiveDateTime>,
|
||||||
|
/// Time the pool was edited
|
||||||
|
pub last_edit_time: Option<NaiveDateTime>,
|
||||||
|
/// The total number of posts the pool has
|
||||||
|
pub post_count: Option<u32>,
|
||||||
|
/// The pool description (instructions how to use, history etc). The client should render
|
||||||
|
/// it as Markdown
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
|
||||||
|
#[builder(setter(strip_option))]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// This type is used when creating or updating a pool object. It uses the builder pattern like so:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use szurubooru_client::models::CreateUpdatePoolBuilder;
|
||||||
|
/// // Create a new pool
|
||||||
|
/// let create_pool = CreateUpdatePoolBuilder::default()
|
||||||
|
/// .names(vec!["foo".to_string(), "bar".to_string()])
|
||||||
|
/// .description("Markdown string".to_string())
|
||||||
|
/// .build()
|
||||||
|
/// .unwrap();
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
pub struct CreateUpdatePool {
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// Names and aliases for this pool. When creating a new pool the first name in this list
|
||||||
|
/// is used as the pool name
|
||||||
|
pub names: Option<Vec<String>>,
|
||||||
|
/// Pool category that this pool belongs to. Must already exist
|
||||||
|
pub category: Option<String>,
|
||||||
|
/// Markdown string describing this pool
|
||||||
|
pub description: Option<String>,
|
||||||
|
/// A list of posts that belong to this pool. The server will throw an error if one of these
|
||||||
|
/// post IDs doesn't exist
|
||||||
|
pub posts: Option<Vec<u32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// This type is used to specify which pools should be merged. Uses the builder pattern like so:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use szurubooru_client::models::MergePoolBuilder;
|
||||||
|
/// // Merge pool ID 1 at version 1 to pool ID 3 at version 5
|
||||||
|
/// let merge_pool = MergePoolBuilder::default()
|
||||||
|
/// .remove_version(1)
|
||||||
|
/// .remove(1)
|
||||||
|
/// .merge_to_version(5)
|
||||||
|
/// .merge_to(3)
|
||||||
|
/// .build()
|
||||||
|
/// .unwrap();
|
||||||
|
/// ```
|
||||||
|
pub struct MergePool {
|
||||||
|
/// Version of the pool to remove. Must match the current Pool version
|
||||||
|
pub remove_version: u32,
|
||||||
|
/// Pool ID to remove
|
||||||
|
pub remove: u32,
|
||||||
|
/// Version of the pool to merge TO
|
||||||
|
pub merge_to_version: u32,
|
||||||
|
/// Pool ID of the pool to merge TO
|
||||||
|
pub merge_to: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A micro resource representing a Pool. A subset of the fields of a [PoolResource].
|
||||||
|
pub struct MicroPoolResource {
|
||||||
|
/// The pool ID
|
||||||
|
pub id: Option<u32>,
|
||||||
|
/// Name and aliases for this pool
|
||||||
|
pub names: Option<Vec<String>>,
|
||||||
|
/// The category this pool belongs to
|
||||||
|
pub category: Option<String>,
|
||||||
|
/// The total number of posts in this pool
|
||||||
|
pub post_count: Option<u32>,
|
||||||
|
/// A markdown string describing the pool
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A type representing a Comment on a post
|
||||||
|
pub struct CommentResource {
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// The comment ID
|
||||||
|
pub id: Option<u32>,
|
||||||
|
/// The post ID this comment belongs to
|
||||||
|
pub post_id: Option<u32>,
|
||||||
|
/// The user who had posted this comment
|
||||||
|
pub user: Option<MicroUserResource>,
|
||||||
|
/// The text of the comment
|
||||||
|
pub text: Option<String>,
|
||||||
|
/// When was the comment posted
|
||||||
|
pub creation_time: Option<NaiveDateTime>,
|
||||||
|
/// When was the last time this comment was edited
|
||||||
|
pub last_edit_time: Option<NaiveDateTime>,
|
||||||
|
/// The sum of the -1/0/+1 scores by other users
|
||||||
|
pub score: Option<i32>,
|
||||||
|
/// The user's own score for this comment
|
||||||
|
pub own_score: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)]
|
||||||
|
#[builder(setter(strip_option))]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// This type is used when creating or updating a comment. This type uses the builder pattern like
|
||||||
|
/// so:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use szurubooru_client::models::CreateUpdateCommentBuilder;
|
||||||
|
/// // Update an existing comment's text for the post ID 1234
|
||||||
|
/// let update_comment = CreateUpdateCommentBuilder::default()
|
||||||
|
/// .version(1)
|
||||||
|
/// .text("Hello this is my comment".to_string())
|
||||||
|
/// .post_id(1234)
|
||||||
|
/// .build()
|
||||||
|
/// .unwrap();
|
||||||
|
/// ```
|
||||||
|
pub struct CreateUpdateComment {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
/// Omitted when creating a new comment
|
||||||
|
pub version: Option<u32>,
|
||||||
|
/// The text of the comment
|
||||||
|
pub text: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// The post the comment should be attached to. Only used when creating a new comment
|
||||||
|
pub post_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// The kind of snapshot that has been recorded
|
||||||
|
pub enum SnapshotOperationType {
|
||||||
|
/// Item was created
|
||||||
|
Created,
|
||||||
|
/// Item was modified
|
||||||
|
Modified,
|
||||||
|
/// Item was deleted
|
||||||
|
Deleted,
|
||||||
|
/// Item was merged
|
||||||
|
Merged,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// The kind of resource described by this snapshot
|
||||||
|
pub enum SnapshotResourceType {
|
||||||
|
/// Tag resource
|
||||||
|
Tag,
|
||||||
|
/// Tag category resource
|
||||||
|
#[serde(rename = "tag_category")]
|
||||||
|
TagCategory,
|
||||||
|
/// Post resource
|
||||||
|
Post,
|
||||||
|
/// Pool resource
|
||||||
|
Pool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase", untagged)]
|
||||||
|
/// Data for a resource that was created
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub enum SnapshotCreationDeletionData {
|
||||||
|
/// A tag resource that was created
|
||||||
|
Tag(TagResource),
|
||||||
|
/// A tag category resource that was created
|
||||||
|
TagCategory(TagCategoryResource),
|
||||||
|
/// A post resource that was created
|
||||||
|
Post(PostResource),
|
||||||
|
/// A pool resource that was created
|
||||||
|
Pool(PoolResource),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// Data for a modified resource
|
||||||
|
pub struct SnapshotModificationData {
|
||||||
|
/// The type of snapshot
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub snapshot_type: String,
|
||||||
|
/// The JSON value for the modified resource. A dictionary diff that depends on the resource
|
||||||
|
/// kind.
|
||||||
|
///
|
||||||
|
/// See [here](https://github.com/rr-/szurubooru/blob/master/doc/API.md#snapshot) for more
|
||||||
|
/// information
|
||||||
|
pub value: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
/// Data for a merged resource
|
||||||
|
pub struct SnapshotMergeData {
|
||||||
|
/// Resource IDs that have been merged
|
||||||
|
pub merged: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
/// Type representing the data as part of a snapshot
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub enum SnapshotData {
|
||||||
|
/// Data for a Created or Deleted resource
|
||||||
|
CreateOrDelete(SnapshotCreationDeletionData),
|
||||||
|
/// Data for a modified resource
|
||||||
|
Modify(SnapshotModificationData),
|
||||||
|
/// Data for a merged resource
|
||||||
|
Merge(SnapshotMergeData),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// Overall type representing some sort of change to a resource
|
||||||
|
pub struct SnapshotResource {
|
||||||
|
/// The operation type
|
||||||
|
pub operation: Option<SnapshotOperationType>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
/// The resource type
|
||||||
|
pub resource_type: Option<SnapshotResourceType>,
|
||||||
|
/// The ID of the snapshot itself
|
||||||
|
pub id: Option<u32>,
|
||||||
|
/// The user who created this change
|
||||||
|
pub user: Option<MicroUserResource>,
|
||||||
|
/// The data associated with this resource change
|
||||||
|
pub data: Option<SnapshotData>,
|
||||||
|
/// When this resource change occurred
|
||||||
|
pub time: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A result when searching for similar posts to a given image
|
||||||
|
pub struct ImageSearchSimilarPost {
|
||||||
|
/// How close the post is to the given image
|
||||||
|
pub distance: f32,
|
||||||
|
/// The post in question
|
||||||
|
pub post: PostResource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// A type to represent the result from an Image search request
|
||||||
|
pub struct ImageSearchResult {
|
||||||
|
/// A post resource that is exact byte-to-byte duplicate of the input file
|
||||||
|
pub exact_post: Option<PostResource>,
|
||||||
|
/// A series of post resources that aren't exact duplicate, but visually resembles
|
||||||
|
/// the input file. Works only on images and animations, does not work for videos and
|
||||||
|
/// Flash movies.
|
||||||
|
pub similar_posts: Vec<ImageSearchSimilarPost>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
/// A type that represents posts that are before or after an existing post
|
||||||
|
pub struct AroundPostResult {
|
||||||
|
/// A previous post, if it exists
|
||||||
|
prev: Option<u32>,
|
||||||
|
/// The next post, if it exists
|
||||||
|
next: Option<u32>,
|
||||||
|
}
|
||||||
72
src/tests.rs
Normal file
72
src/tests.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::client::SzurubooruClient;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::models::*;
|
||||||
|
use mockito::Server;
|
||||||
|
use serde_json::{json, to_string};
|
||||||
|
|
||||||
|
static TEST_AUTH: &str = "Token dGVzdDp0ZXN0";
|
||||||
|
static TEST2_AUTH: &str = "Token dGVzdDI6dGVzdAo=";
|
||||||
|
static ADMIN_AUTH: &str = "Token YWRtaW46dGVzdA==";
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_tag_categories() -> SzurubooruResult<()> {
|
||||||
|
let mut server = Server::new_async().await;
|
||||||
|
|
||||||
|
let test_client = SzurubooruClient::new_with_token(&server.url(), "admin", "test", true)?;
|
||||||
|
let admin_get_tcs = server
|
||||||
|
.mock("GET", "/tag-categories")
|
||||||
|
.match_header("Authorization", ADMIN_AUTH)
|
||||||
|
.with_body(
|
||||||
|
to_string(&json!({
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"version": 0,
|
||||||
|
"name": "my-tag-category",
|
||||||
|
"color": "blue",
|
||||||
|
"usages": 1,
|
||||||
|
"order": "asc",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
let tc_res = test_client.request().list_tag_categories().await?;
|
||||||
|
admin_get_tcs.assert_async().await;
|
||||||
|
assert_eq!(tc_res.results.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
tc_res.results.first().unwrap().color,
|
||||||
|
Some("blue".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
let fail_tcs = server
|
||||||
|
.mock("GET", "/tag-categories")
|
||||||
|
.match_header("Authorization", TEST_AUTH)
|
||||||
|
.with_body(
|
||||||
|
to_string(&json!({
|
||||||
|
"name": "AuthError",
|
||||||
|
"title": "Auth Error",
|
||||||
|
"description": "Authentication error"
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let test_client = SzurubooruClient::new_with_token(&server.url(), "test", "test", true)?;
|
||||||
|
let failed_tc_res = test_client.request().list_tag_categories().await;
|
||||||
|
fail_tcs.assert_async().await;
|
||||||
|
match failed_tc_res {
|
||||||
|
Err(SzurubooruClientError::SzurubooruServerError(e)) => {
|
||||||
|
assert_eq!(e.name, SzurubooruServerErrorType::AuthError)
|
||||||
|
}
|
||||||
|
_ => assert!(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
617
src/tokens.rs
Normal file
617
src/tokens.rs
Normal file
|
|
@ -0,0 +1,617 @@
|
||||||
|
//! Types that represent various Query tokens for the API endpoints that support them. Be
|
||||||
|
//! warned that the types here help with the Type safety for the Tag names only. It does
|
||||||
|
//! not guarantee that a given API endpoint will support the given tag.
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
use strum_macros::AsRefStr;
|
||||||
|
use urlencoding::encode;
|
||||||
|
|
||||||
|
/// A named token such as `foo:bar`
|
||||||
|
pub trait NamedToken: AsRef<str> {}
|
||||||
|
|
||||||
|
/// A type of token used for sorting. E.g: `sort:random`
|
||||||
|
pub trait SortableToken: AsRef<str> {}
|
||||||
|
|
||||||
|
/// Special tokens such as `liked` posts or `tumbleweed` that
|
||||||
|
/// don't fit into a query token or sort token
|
||||||
|
pub trait SpecialToken: AsRef<str> {}
|
||||||
|
|
||||||
|
/// Supports types that can be converted to a Query string
|
||||||
|
pub trait ToQueryString {
|
||||||
|
/// Convert `&self` into a HTML query string
|
||||||
|
fn to_query_string(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A query token using for searching posts, tags and pools
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct QueryToken {
|
||||||
|
/// The key for this token. For `foo:bar` this would be `foo`
|
||||||
|
pub key: String,
|
||||||
|
/// The value for this token. For `foo:bar` this would be `bar`
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryToken {
|
||||||
|
///
|
||||||
|
/// Construct a named token for a search query. Final results takes the form of
|
||||||
|
/// `key:value`. Values containing `:` and `-` are automatically escaped.
|
||||||
|
///
|
||||||
|
/// `key` can either be one of the existing [NamedToken] types for convenience, or anything
|
||||||
|
/// that implements [`AsRef<str>`] for custom tokens.
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use szurubooru_client::SzurubooruClient;
|
||||||
|
/// # let client = SzurubooruClient::new_with_token("http://foo", "user", "pwd", true).unwrap();
|
||||||
|
/// // let client = SzurubooruClient::new(...)
|
||||||
|
/// use szurubooru_client::tokens::{PostNamedToken, QueryToken};
|
||||||
|
/// // Find all posts with at least one comment...
|
||||||
|
/// let qt = QueryToken::token(PostNamedToken::CommentCount, "0..");
|
||||||
|
/// // ...with a positive score.
|
||||||
|
/// let custom = QueryToken::token("score", "0..");
|
||||||
|
/// client.request().list_posts(Some(&vec![qt, custom]));
|
||||||
|
/// ```
|
||||||
|
pub fn token(key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
|
||||||
|
let escaped = value.as_ref().replace(":", "\\:").replace("-", "\\-");
|
||||||
|
Self {
|
||||||
|
key: key.as_ref().to_string(),
|
||||||
|
value: escaped,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Constructs a token for sorting purposes. Final results take the form of
|
||||||
|
/// `sort:value`.
|
||||||
|
///
|
||||||
|
/// `value` can either be one of the existing [SortableToken] types for convenience or any type
|
||||||
|
/// that implements [`AsRef<str>`]
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use szurubooru_client::SzurubooruClient;
|
||||||
|
/// # let client = SzurubooruClient::new_with_token("http://foo", "user", "pwd", true).unwrap();
|
||||||
|
/// // let client = SzurubooruClient::new(...)
|
||||||
|
/// use szurubooru_client::tokens::{PostSortToken, QueryToken};
|
||||||
|
/// // Sort posts at random
|
||||||
|
/// let sort_token = QueryToken::sort(PostSortToken::Random);
|
||||||
|
/// client.request().list_posts(Some(&vec![sort_token]));
|
||||||
|
/// ```
|
||||||
|
pub fn sort(value: impl AsRef<str>) -> Self {
|
||||||
|
Self {
|
||||||
|
key: "sort".to_string(),
|
||||||
|
value: value.as_ref().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Constructs a new anonymous token. These are resource specific, e.g for [crate::models::PostResource] it's
|
||||||
|
/// the same as [PostNamedToken::Tag].
|
||||||
|
///
|
||||||
|
/// Keys containing `:` and `-` are automatically escaped.
|
||||||
|
/// ```no_run
|
||||||
|
/// # use szurubooru_client::SzurubooruClient;
|
||||||
|
/// # let client = SzurubooruClient::new_with_token("http://foo", "user", "pwd", true).unwrap();
|
||||||
|
/// // let client = SzurubooruClient::new(...)
|
||||||
|
/// use szurubooru_client::tokens::QueryToken;
|
||||||
|
/// // Fetch all posts containing the tag "re:zero"
|
||||||
|
/// // Tag will be escaped as "re\:zero"
|
||||||
|
/// let re_zero = QueryToken::anonymous("re:zero");
|
||||||
|
/// client.request().list_posts(Some(&vec![re_zero]));
|
||||||
|
/// ```
|
||||||
|
pub fn anonymous(key: impl AsRef<str>) -> Self {
|
||||||
|
let escaped = key.as_ref().replace(":", "\\:").replace("-", "\\-");
|
||||||
|
Self {
|
||||||
|
key: escaped,
|
||||||
|
value: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Constructs a new special token. Some resource types (see [PostSpecialToken]) support
|
||||||
|
/// special tokens. This is a convenience function for `QueryToken::anonymous` that provides
|
||||||
|
/// type-safe construction of a QueryToken.
|
||||||
|
/// ```no_run
|
||||||
|
/// # use szurubooru_client::SzurubooruClient;
|
||||||
|
/// use szurubooru_client::tokens::{PostSpecialToken, QueryToken};
|
||||||
|
/// # let client = SzurubooruClient::new_with_token("http://foo", "user", "pwd", true).unwrap();
|
||||||
|
/// // let client = SzurubooruClient::new(...)
|
||||||
|
/// // Return posts liked by the current authenticated user
|
||||||
|
/// let liked_posts = QueryToken::special(PostSpecialToken::Liked);
|
||||||
|
/// client.request().list_posts(Some(&vec![liked_posts]));
|
||||||
|
/// ```
|
||||||
|
pub fn special(key: impl SpecialToken) -> Self {
|
||||||
|
QueryToken::anonymous(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Negate the existing token. Include becomes Exclude and vice versa.
|
||||||
|
///
|
||||||
|
/// E.g: `konosuba` becomes `-konosuba`
|
||||||
|
///
|
||||||
|
pub fn negate(&self) -> Self {
|
||||||
|
let negated_key = if self.key.starts_with("-") {
|
||||||
|
self.key[1..].to_string()
|
||||||
|
} else {
|
||||||
|
format!("-{}", self.key)
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
key: negated_key,
|
||||||
|
value: self.value.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for QueryToken {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let suffix = if !self.value.is_empty() {
|
||||||
|
format!(":{}", self.value)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
write!(f, "{}{}", self.key, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToQueryString for Vec<QueryToken> {
|
||||||
|
fn to_query_string(&self) -> String {
|
||||||
|
let query_vec: Vec<String> = self.iter().map(|qv| qv.to_string()).collect();
|
||||||
|
encode(&query_vec.join(" ")).to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe named query tokens for use with [list_tags](crate::SzurubooruRequest::list_tags)
|
||||||
|
pub enum TagNamedToken {
|
||||||
|
/// having given name (accepts wildcards)
|
||||||
|
Name,
|
||||||
|
/// having given category (accepts wildcards)
|
||||||
|
Category,
|
||||||
|
/// created at given date
|
||||||
|
CreationDate,
|
||||||
|
/// edited at given date
|
||||||
|
LastEditDate,
|
||||||
|
/// alias of [TagNamedToken::LastEditTime]
|
||||||
|
LastEditTime,
|
||||||
|
/// alias of [TagNamedToken::LastEditTime]
|
||||||
|
EditDate,
|
||||||
|
/// alias of [TagNamedToken::LastEditTime]
|
||||||
|
EditTime,
|
||||||
|
/// used in given number of posts
|
||||||
|
Usages,
|
||||||
|
/// alias of [TagNamedToken::Usages]
|
||||||
|
UsageCount,
|
||||||
|
/// alias of [TagNamedToken::Usages]
|
||||||
|
PostCount,
|
||||||
|
/// with given number of suggestions
|
||||||
|
SuggestionCount,
|
||||||
|
/// with given number of implications
|
||||||
|
ImplicationCount,
|
||||||
|
}
|
||||||
|
impl NamedToken for TagNamedToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe sort query tokens for use with [list_tags](crate::SzurubooruRequest::list_tags)
|
||||||
|
pub enum TagSortToken {
|
||||||
|
/// as random as it can get
|
||||||
|
Random,
|
||||||
|
/// A to Z
|
||||||
|
Name,
|
||||||
|
/// category (A to Z)
|
||||||
|
Category,
|
||||||
|
/// recently created first
|
||||||
|
CreationDate,
|
||||||
|
/// alias of [TagSortToken::CreationDate]
|
||||||
|
CreationTime,
|
||||||
|
/// recently edited first
|
||||||
|
LastEditDate,
|
||||||
|
/// alias of [TagSortToken::CreationTime]
|
||||||
|
LastEditTime,
|
||||||
|
/// alias of [TagSortToken::CreationTime]
|
||||||
|
EditDate,
|
||||||
|
/// alias of [TagSortToken::CreationTime]
|
||||||
|
EditTime,
|
||||||
|
/// used in most posts first
|
||||||
|
Usages,
|
||||||
|
/// alias of [TagSortToken::Usages]
|
||||||
|
UsageCount,
|
||||||
|
/// alias of [TagSortToken::Usages]
|
||||||
|
PostCount,
|
||||||
|
/// with most suggestions first
|
||||||
|
SuggestionCount,
|
||||||
|
/// with most implications first
|
||||||
|
ImplicationCount,
|
||||||
|
}
|
||||||
|
impl SortableToken for TagSortToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe named query tokens for use with [list_posts](crate::SzurubooruRequest::list_posts)
|
||||||
|
pub enum PostNamedToken {
|
||||||
|
/// having given post number
|
||||||
|
Id,
|
||||||
|
/// having given tag (accepts wildcards)
|
||||||
|
Tag,
|
||||||
|
/// having given score
|
||||||
|
Score,
|
||||||
|
/// uploaded by given user (accepts wildcards)
|
||||||
|
Uploader,
|
||||||
|
/// alias of [PostNamedToken::Uploader]
|
||||||
|
Upload,
|
||||||
|
/// alias of [PostNamedToken::Uploader]
|
||||||
|
Submit,
|
||||||
|
/// commented by given user (accepts wildcards)
|
||||||
|
Comment,
|
||||||
|
/// favorited by given user (accepts wildcards)
|
||||||
|
Fav,
|
||||||
|
/// belonging to the pool with the given ID
|
||||||
|
Pool,
|
||||||
|
/// having given number of tags
|
||||||
|
TagCount,
|
||||||
|
/// having given number of comments
|
||||||
|
CommentCount,
|
||||||
|
/// favorited by given number of users
|
||||||
|
FavCount,
|
||||||
|
/// having given number of annotations
|
||||||
|
NoteCount,
|
||||||
|
/// having given note text (accepts wildcards)
|
||||||
|
NoteText,
|
||||||
|
/// having given number of relations
|
||||||
|
RelationCount,
|
||||||
|
/// having been featured given number of times
|
||||||
|
FeatureCount,
|
||||||
|
/// given type of posts. `value` can be either `image`, `animation` (or `animated` or `anim`),
|
||||||
|
/// `flash` (or `swf`) or `video` (or `webm`). Use [models::PostType] for type-safe values
|
||||||
|
Type,
|
||||||
|
/// having given SHA1 checksum
|
||||||
|
ContentChecksum,
|
||||||
|
/// having given file size (in bytes)
|
||||||
|
FileSize,
|
||||||
|
/// having given image width (where applicable)
|
||||||
|
ImageWidth,
|
||||||
|
/// having given image height (where applicable)
|
||||||
|
ImageHeight,
|
||||||
|
/// having given number of pixels (image width * image height)
|
||||||
|
ImageArea,
|
||||||
|
/// having given aspect ratio (image width / image height)
|
||||||
|
ImageAspectRatio,
|
||||||
|
/// alias of [PostNamedToken::ImageAspectRatio]
|
||||||
|
ImageAr,
|
||||||
|
/// alias of [PostNamedToken::ImageWidth]
|
||||||
|
Width,
|
||||||
|
/// alias of [PostNamedToken::ImageHeight]
|
||||||
|
Height,
|
||||||
|
/// alias of [PostNamedToken::ImageAspectRatio]
|
||||||
|
Ar,
|
||||||
|
/// alias of [PostNamedToken::ImageAspectRatio]
|
||||||
|
AspectRatio,
|
||||||
|
/// posted at given date
|
||||||
|
CreationDate,
|
||||||
|
/// alias of [PostNamedToken::CreationDate]
|
||||||
|
CreationTime,
|
||||||
|
/// alias of [PostNamedToken::CreationDate]
|
||||||
|
Date,
|
||||||
|
/// alias of [PostNamedToken::CreationDate]
|
||||||
|
Time,
|
||||||
|
/// edited at given date
|
||||||
|
LastEditDate,
|
||||||
|
/// alias of [PostNamedToken::LastEditDate]
|
||||||
|
LastEditTime,
|
||||||
|
/// alias of [PostNamedToken::LastEditDate]
|
||||||
|
EditDate,
|
||||||
|
/// alias of [PostNamedToken::LastEditDate]
|
||||||
|
EditTime,
|
||||||
|
/// commented at given date
|
||||||
|
CommentDate,
|
||||||
|
/// alias of [PostNamedToken::CommentDate]
|
||||||
|
CommentTime,
|
||||||
|
/// last favorited at given time
|
||||||
|
FavDate,
|
||||||
|
/// alias of [PostNamedToken::FavDate]
|
||||||
|
FavTime,
|
||||||
|
/// featured at given date
|
||||||
|
FeatureDate,
|
||||||
|
/// alias of [PostNamedToken::FeatureDate]
|
||||||
|
FeatureTime,
|
||||||
|
/// having given safety. <value> can be either `safe`, `sketchy` (or `questionable`) or `unsafe`
|
||||||
|
/// Use [models::PostSafety] for the type-safe version
|
||||||
|
Safety,
|
||||||
|
/// alias of [PostNamedToken::Safety]
|
||||||
|
Rating,
|
||||||
|
}
|
||||||
|
impl NamedToken for PostNamedToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe sort query tokens for use with [list_posts](crate::SzurubooruRequest::list_posts)
|
||||||
|
pub enum PostSortToken {
|
||||||
|
/// as random as it can get
|
||||||
|
Random,
|
||||||
|
/// highest to lowest post number
|
||||||
|
Id,
|
||||||
|
/// highest scored
|
||||||
|
Score,
|
||||||
|
/// with most tags
|
||||||
|
TagCount,
|
||||||
|
/// most commented first
|
||||||
|
CommentCount,
|
||||||
|
/// loved by most
|
||||||
|
FavCount,
|
||||||
|
/// with most annotations
|
||||||
|
NoteCount,
|
||||||
|
/// with most relations
|
||||||
|
RelationCount,
|
||||||
|
/// most often featured
|
||||||
|
FeatureCount,
|
||||||
|
/// largest files first
|
||||||
|
FileSize,
|
||||||
|
/// widest images first
|
||||||
|
ImageWidth,
|
||||||
|
/// tallest images first
|
||||||
|
ImageHeight,
|
||||||
|
/// largest images first
|
||||||
|
ImageArea,
|
||||||
|
/// alias of [PostSortToken::ImageWidth]
|
||||||
|
Width,
|
||||||
|
/// alias of [PostSortToken::ImageHeight]
|
||||||
|
Height,
|
||||||
|
/// alias of [PostSortToken::ImageArea]
|
||||||
|
Area,
|
||||||
|
/// newest to oldest (pretty much same as id)
|
||||||
|
CreationDate,
|
||||||
|
/// alias of [PostSortToken::CreationDate]
|
||||||
|
CreationTime,
|
||||||
|
/// alias of [PostSortToken::CreationDate]
|
||||||
|
Date,
|
||||||
|
/// alias of [PostSortToken::CreationDate]
|
||||||
|
Time,
|
||||||
|
/// like [PostSortToken::CreationDate], only looks at last edit time
|
||||||
|
LastEditDate,
|
||||||
|
/// alias of [PostSortToken::LastEditDate]
|
||||||
|
LastEditTime,
|
||||||
|
/// alias of [PostSortToken::LastEditDate]
|
||||||
|
EditDate,
|
||||||
|
/// alias of [PostSortToken::LastEditDate]
|
||||||
|
EditTime,
|
||||||
|
/// recently commented by anyone
|
||||||
|
CommentDate,
|
||||||
|
/// alias of [PostSortToken::CommentDate]
|
||||||
|
CommentTime,
|
||||||
|
/// recently added to favorites by anyone
|
||||||
|
FavDate,
|
||||||
|
/// alias of [PostSortToken::FavDate]
|
||||||
|
FavTime,
|
||||||
|
/// recently featured
|
||||||
|
FeatureDate,
|
||||||
|
/// alias of [PostSortToken::FeatureDate]
|
||||||
|
FeatureTime,
|
||||||
|
}
|
||||||
|
impl SortableToken for PostSortToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe special query tokens for use with [list_posts](crate::SzurubooruRequest::list_posts)
|
||||||
|
pub enum PostSpecialToken {
|
||||||
|
/// posts liked by currently logged-in user
|
||||||
|
Liked,
|
||||||
|
/// posts disliked by currently logged in user
|
||||||
|
Disliked,
|
||||||
|
/// posts added to favorites by currently logged-in user
|
||||||
|
Fav,
|
||||||
|
/// posts with score of 0, without comments and without favorites
|
||||||
|
Tumbleweed,
|
||||||
|
}
|
||||||
|
impl SpecialToken for PostSpecialToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe named query tokens for use with [list_pools](crate::SzurubooruRequest::list_pools)
|
||||||
|
pub enum PoolNamedToken {
|
||||||
|
/// having given name (accepts wildcards)
|
||||||
|
Name,
|
||||||
|
/// having given category (accepts wildcards)
|
||||||
|
Category,
|
||||||
|
/// created at given date
|
||||||
|
CreationDate,
|
||||||
|
/// alias of [CreationDate](PoolNamedToken::CreationDate)
|
||||||
|
CreationTime,
|
||||||
|
/// edited at given date
|
||||||
|
LastEditDate,
|
||||||
|
/// alias of [LastEditDate](PoolNamedToken::LastEditDate)
|
||||||
|
LastEditTime,
|
||||||
|
/// alias of [LastEditDate](PoolNamedToken::LastEditDate)
|
||||||
|
EditDate,
|
||||||
|
/// alias of [LastEditDate](PoolNamedToken::LastEditDate)
|
||||||
|
EditTime,
|
||||||
|
/// used in given number of posts
|
||||||
|
PostCount,
|
||||||
|
}
|
||||||
|
impl NamedToken for PoolNamedToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe sort query tokens for use with [list_pools](crate::SzurubooruRequest::list_pools)
|
||||||
|
pub enum PoolSortToken {
|
||||||
|
/// as random as it can get
|
||||||
|
Random,
|
||||||
|
/// A to Z
|
||||||
|
Name,
|
||||||
|
/// category (A to Z)
|
||||||
|
Category,
|
||||||
|
/// recently created first
|
||||||
|
CreationDate,
|
||||||
|
/// alias of [CreationDate](PoolSortToken::CreationDate)
|
||||||
|
CreationTime,
|
||||||
|
/// recently edited first
|
||||||
|
LastEditDate,
|
||||||
|
/// alias of [CreationDate](PoolSortToken::LastEditDate)
|
||||||
|
LastEditTime,
|
||||||
|
/// alias of [CreationDate](PoolSortToken::LastEditDate)
|
||||||
|
EditDate,
|
||||||
|
/// alias of [CreationDate](PoolSortToken::LastEditDate)
|
||||||
|
EditTime,
|
||||||
|
/// used in most posts first
|
||||||
|
PostCount,
|
||||||
|
}
|
||||||
|
impl SortableToken for PoolSortToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe named query tokens for use with
|
||||||
|
/// [list_comments](crate::SzurubooruRequest::list_comments)
|
||||||
|
pub enum CommentNamedToken {
|
||||||
|
/// specific comment ID
|
||||||
|
Id,
|
||||||
|
/// specific post ID
|
||||||
|
Post,
|
||||||
|
/// created by given user (accepts wildcards)
|
||||||
|
User,
|
||||||
|
/// alias of user
|
||||||
|
Author,
|
||||||
|
/// containing given text (accepts wildcards)
|
||||||
|
Text,
|
||||||
|
/// created at given date
|
||||||
|
CreationDate,
|
||||||
|
/// alias of creation-date
|
||||||
|
CreationTime,
|
||||||
|
/// whose most recent edit date matches given date
|
||||||
|
LastEditDate,
|
||||||
|
/// alias of last-edit-date
|
||||||
|
LastEditTime,
|
||||||
|
/// alias of last-edit-date
|
||||||
|
EditDate,
|
||||||
|
/// alias of last-edit-date
|
||||||
|
EditTime,
|
||||||
|
}
|
||||||
|
impl NamedToken for CommentNamedToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe sort query tokens for use with
|
||||||
|
/// [list_comments](crate::SzurubooruRequest::list_comments)
|
||||||
|
pub enum CommentSortToken {
|
||||||
|
/// as random as it can get
|
||||||
|
Random,
|
||||||
|
/// author name, A to Z
|
||||||
|
User,
|
||||||
|
/// alias of user
|
||||||
|
Author,
|
||||||
|
/// post ID, newest to oldest
|
||||||
|
Post,
|
||||||
|
/// newest to oldest
|
||||||
|
CreationDate,
|
||||||
|
/// alias of creation-date
|
||||||
|
CreationTime,
|
||||||
|
/// recently edited first
|
||||||
|
LastEditDate,
|
||||||
|
/// alias of last-edit-date
|
||||||
|
LastEditTime,
|
||||||
|
/// alias of last-edit-date
|
||||||
|
EditDate,
|
||||||
|
/// alias of last-edit-date
|
||||||
|
EditTime,
|
||||||
|
}
|
||||||
|
impl SortableToken for CommentSortToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe named query tokens for use with [list_users](crate::SzurubooruRequest::list_users)
|
||||||
|
pub enum UserNamedToken {
|
||||||
|
/// having given name (accepts wildcards)
|
||||||
|
Name,
|
||||||
|
/// registered at given date
|
||||||
|
CreationDate,
|
||||||
|
/// alias of [CreationDate](UserNamedToken::CreationDate)
|
||||||
|
CreationTime,
|
||||||
|
/// whose most recent login date matches given date
|
||||||
|
LastLoginDate,
|
||||||
|
/// alias of [LastLoginDate](UserNamedToken::LastLoginDate)
|
||||||
|
LastLoginTime,
|
||||||
|
/// alias of [LastLoginDate](UserNamedToken::LastLoginDate)
|
||||||
|
LoginDate,
|
||||||
|
/// alias of [LastLoginDate](UserNamedToken::LastLoginDate)
|
||||||
|
LoginTime,
|
||||||
|
}
|
||||||
|
impl NamedToken for UserNamedToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe sort query tokens for use with [list_users](crate::SzurubooruRequest::list_users)
|
||||||
|
pub enum UserSortToken {
|
||||||
|
/// as random as it can get
|
||||||
|
Random,
|
||||||
|
/// A to Z
|
||||||
|
Name,
|
||||||
|
/// newest to oldest
|
||||||
|
CreationDate,
|
||||||
|
/// alias of [CreationDate](UserSortToken::CreationDate)
|
||||||
|
CreationTime,
|
||||||
|
/// recently active first
|
||||||
|
LastLoginDate,
|
||||||
|
/// alias of [LastLoginDate](UserSortToken::LastLoginDate)
|
||||||
|
LastLoginTime,
|
||||||
|
/// alias of [LastLoginDate](UserSortToken::LastLoginDate)
|
||||||
|
LoginDate,
|
||||||
|
/// alias of [LastLoginDate](UserSortToken::LastLoginDate)
|
||||||
|
LoginTime,
|
||||||
|
}
|
||||||
|
impl SortableToken for UserNamedToken {}
|
||||||
|
|
||||||
|
#[derive(Debug, AsRefStr)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
/// Type-safe named query tokens for use with
|
||||||
|
/// [list_snapshots](crate::SzurubooruRequest::list_snapshots)
|
||||||
|
pub enum SnapshotNamedToken {
|
||||||
|
/// involving given resource type
|
||||||
|
Type,
|
||||||
|
/// involving given resource id
|
||||||
|
Id,
|
||||||
|
/// created at given date
|
||||||
|
Date,
|
||||||
|
/// alias of date
|
||||||
|
Time,
|
||||||
|
/// modified, created, deleted or merged
|
||||||
|
Operation,
|
||||||
|
/// name of the user that created given snapshot (accepts wildcards)
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
impl NamedToken for SnapshotNamedToken {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::tokens::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_query_token() {
|
||||||
|
let qt = QueryToken::token(PostNamedToken::CommentCount, "1");
|
||||||
|
assert_eq!(qt.to_string(), "comment-count:1");
|
||||||
|
|
||||||
|
let qt = qt.negate();
|
||||||
|
assert_eq!(qt.to_string(), "-comment-count:1");
|
||||||
|
|
||||||
|
let qt = QueryToken::sort(PostSortToken::Random);
|
||||||
|
assert_eq!(qt.to_string(), "sort:random");
|
||||||
|
|
||||||
|
let qt = QueryToken::token(TagNamedToken::Name, "re:zero");
|
||||||
|
assert_eq!(qt.to_string(), r#"name:re\:zero"#);
|
||||||
|
|
||||||
|
let qt = QueryToken::special(PostSpecialToken::Liked);
|
||||||
|
assert_eq!(qt.to_string(), "liked");
|
||||||
|
|
||||||
|
let qt = QueryToken::anonymous("foo");
|
||||||
|
assert_eq!(qt.to_string(), "foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vec_query() {
|
||||||
|
let query_vec = vec![
|
||||||
|
QueryToken::token(PostNamedToken::CommentCount, "1"),
|
||||||
|
QueryToken::sort(PostSortToken::Random),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
query_vec.to_query_string(),
|
||||||
|
"comment-count%3A1%20sort%3Arandom"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue