From a74cabcbccc4f72d525499384366e3c2bdedc6c6 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 11:34:43 +0100 Subject: [PATCH 01/11] Add dotenv support for tests Signed-off-by: trivernis --- .gitignore | 1 + Cargo.toml | 1 + tests/common.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 96ef6c0..3549fae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +.env \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 4d51952..14d1ef6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ lazy_static = "1.4.0" [dev-dependencies] env_logger = "0.8.4" maplit = "1.0.2" +dotenv = "0.15.0" [dev-dependencies.tokio] version = "1.8.0" diff --git a/tests/common.rs b/tests/common.rs index dba8290..576bfef 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -8,6 +8,7 @@ use std::sync::Arc; pub fn setup() { lazy_static::lazy_static! { static ref SETUP_DONE: Arc = Arc::new(AtomicBool::new(false)); } if !SETUP_DONE.swap(true, Ordering::SeqCst) { + dotenv::dotenv().expect("failed to initialize dotenv"); env_logger::builder() .filter_level(LevelFilter::Trace) .init(); From 8bb7149e116b9bd210360ff66e03db6e09652913 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 12:02:30 +0100 Subject: [PATCH 02/11] Remove log and use tracing instead Signed-off-by: trivernis --- Cargo.toml | 4 +- src/api_core/client.rs | 116 ++++++++++++++++++++++------------------ src/api_core/mod.rs | 5 +- src/wrapper/hydrus.rs | 9 ++-- src/wrapper/or_chain.rs | 2 +- tests/common.rs | 5 +- 6 files changed, 75 insertions(+), 66 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 14d1ef6..089e0ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,16 +13,16 @@ repository = "https://github.com/trivernis/hydrus-api-rs" [dependencies] serde = {version = "^1.0", features = ["derive"]} reqwest = {version = "0.11.4", features = ["json"]} -log = "0.4.14" +tracing = "0.1.31" mime = "0.3.16" chrono = "0.4.19" regex = "1.5.4" lazy_static = "1.4.0" [dev-dependencies] -env_logger = "0.8.4" maplit = "1.0.2" dotenv = "0.15.0" +tracing-subscriber = "0.3.9" [dev-dependencies.tokio] version = "1.8.0" diff --git a/src/api_core/client.rs b/src/api_core/client.rs index 1315ba4..7905356 100644 --- a/src/api_core/client.rs +++ b/src/api_core/client.rs @@ -32,6 +32,7 @@ use crate::utils::{ use reqwest::Response; use serde::de::DeserializeOwned; use serde::Serialize; +use std::fmt::Debug; static ACCESS_KEY_HEADER: &str = "Hydrus-Client-API-Access-Key"; @@ -56,8 +57,9 @@ impl Client { } /// Starts a get request to the path - async fn get(&self, query: &Q) -> Result { - log::debug!("GET request to {}", E::path()); + #[tracing::instrument(skip(self), level = "trace")] + async fn get(&self, query: &Q) -> Result { + tracing::trace!("GET request to {}", E::path()); let response = self .inner .get(format!("{}/{}", self.base_url, E::path())) @@ -70,7 +72,8 @@ impl Client { } /// Starts a get request to the path associated with the Endpoint Type - async fn get_and_parse( + #[tracing::instrument(skip(self), level = "trace")] + async fn get_and_parse( &self, query: &Q, ) -> Result { @@ -80,8 +83,9 @@ impl Client { } /// Stats a post request to the path associated with the Endpoint Type + #[tracing::instrument(skip(self), level = "trace")] async fn post(&self, body: E::Request) -> Result { - log::debug!("POST request to {}", E::path()); + tracing::trace!("POST request to {}", E::path()); let response = self .inner .post(format!("{}/{}", self.base_url, E::path())) @@ -94,6 +98,7 @@ impl Client { } /// Stats a post request and parses the body as json + #[tracing::instrument(skip(self), level = "trace")] async fn post_and_parse(&self, body: E::Request) -> Result { let response = self.post::(body).await?; @@ -101,8 +106,9 @@ impl Client { } /// Stats a post request to the path associated with the return type + #[tracing::instrument(skip(self, data), level = "trace")] async fn post_binary(&self, data: Vec) -> Result { - log::debug!("Binary POST request to {}", E::path()); + tracing::trace!("Binary POST request to {}", E::path()); let response = self .inner .post(format!("{}/{}", self.base_url, E::path())) @@ -117,10 +123,11 @@ impl Client { } /// Returns an error with the response text content if the status doesn't indicate success + #[tracing::instrument(level = "trace")] async fn extract_error(response: Response) -> Result { if !response.status().is_success() { let msg = response.text().await?; - log::error!("API returned error '{}'", msg); + tracing::error!("API returned error '{}'", msg); Err(Error::Hydrus(msg)) } else { Ok(response) @@ -128,51 +135,55 @@ impl Client { } /// Parses the response as JSOn - async fn extract_content(response: Response) -> Result { - response.json::().await.map_err(Error::from) + #[tracing::instrument(level = "trace")] + async fn extract_content(response: Response) -> Result { + let content = response.json::().await?; + tracing::trace!("response content: {:?}", content); + + Ok(content) } /// Returns the current API version. It's being incremented every time the API changes. + #[tracing::instrument(skip(self), level = "debug")] pub async fn api_version(&self) -> Result { - log::trace!("Getting api version"); self.get_and_parse::(&()).await } /// Creates a new session key + #[tracing::instrument(skip(self), level = "debug")] pub async fn session_key(&self) -> Result { - log::trace!("Getting session key"); self.get_and_parse::(&()).await } /// Verifies if the access key is valid and returns some information about its permissions + #[tracing::instrument(skip(self), level = "debug")] pub async fn verify_access_key(&self) -> Result { - log::trace!("Verifying access key"); self.get_and_parse::(&()).await } /// Returns the list of tag and file services of the client + #[tracing::instrument(skip(self), level = "debug")] pub async fn get_services(&self) -> Result { - log::trace!("Getting services"); self.get_and_parse::(&()).await } /// Adds a file to hydrus - pub async fn add_file(&self, path: S) -> Result { + #[tracing::instrument(skip(self), level = "debug")] + pub async fn add_file(&self, path: S) -> Result { let path = path.to_string(); - log::trace!("Adding file {}", path); self.post_and_parse::(AddFileRequest { path }) .await } /// Adds a file from binary data to hydrus + #[tracing::instrument(skip(self, data), level = "debug")] pub async fn add_binary_file(&self, data: Vec) -> Result { - log::trace!("Adding binary file"); self.post_binary::(data).await } /// Moves files with matching hashes to the trash + #[tracing::instrument(skip(self), level = "debug")] pub async fn delete_files(&self, hashes: Vec) -> Result<()> { - log::trace!("Deleting files {:?}", hashes); self.post::(DeleteFilesRequest { hashes }) .await?; @@ -180,8 +191,8 @@ impl Client { } /// Pulls files out of the trash by hash + #[tracing::instrument(skip(self), level = "debug")] pub async fn undelete_files(&self, hashes: Vec) -> Result<()> { - log::trace!("Undeleting files {:?}", hashes); self.post::(UndeleteFilesRequest { hashes }) .await?; @@ -189,8 +200,8 @@ impl Client { } /// Moves files from the inbox into the archive + #[tracing::instrument(skip(self), level = "debug")] pub async fn archive_files(&self, hashes: Vec) -> Result<()> { - log::trace!("Archiving files {:?}", hashes); self.post::(ArchiveFilesRequest { hashes }) .await?; @@ -198,8 +209,8 @@ impl Client { } /// Moves files from the archive into the inbox + #[tracing::instrument(skip(self), level = "debug")] pub async fn unarchive_files(&self, hashes: Vec) -> Result<()> { - log::trace!("Unarchiving files {:?}", hashes); self.post::(UnarchiveFilesRequest { hashes }) .await?; @@ -207,8 +218,8 @@ impl Client { } /// Returns the list of tags as the client would see them in a human friendly order + #[tracing::instrument(skip(self), level = "debug")] pub async fn clean_tags(&self, tags: Vec) -> Result { - log::trace!("Cleaning tags {:?}", tags); self.get_and_parse::(&[( "tags", string_list_to_json_array(tags), @@ -218,19 +229,18 @@ impl Client { /// Adds tags to files with the given hashes pub async fn add_tags(&self, request: AddTagsRequest) -> Result<()> { - log::trace!("Adding tags {:?}", request); self.post::(request).await?; Ok(()) } /// Searches for files in the inbox, the archive or both + #[tracing::instrument(skip(self), level = "debug")] pub async fn search_files( &self, query: Vec, options: FileSearchOptions, ) -> Result { - log::trace!("Searching for files with tags {:?}", query); let mut args = options.into_query_args(); args.push(("tags", search_query_list_to_json_array(query))); self.get_and_parse::(&args) @@ -238,16 +248,12 @@ impl Client { } /// Returns the metadata for a given list of file_ids or hashes + #[tracing::instrument(skip(self), level = "debug")] pub async fn get_file_metadata( &self, file_ids: Vec, hashes: Vec, ) -> Result { - log::trace!( - "Getting file info for ids {:?} or hashes {:?}", - file_ids, - hashes - ); let query = if file_ids.len() > 0 { ("file_ids", number_list_to_json_array(file_ids)) } else { @@ -258,11 +264,11 @@ impl Client { } /// Returns the metadata for a single file identifier + #[tracing::instrument(skip(self), level = "debug")] pub async fn get_file_metadata_by_identifier( &self, id: FileIdentifier, ) -> Result { - log::trace!("Getting file metadata {:?}", id); let mut response = match id.clone() { FileIdentifier::ID(id) => self.get_file_metadata(vec![id], vec![]).await?, FileIdentifier::Hash(hash) => self.get_file_metadata(vec![], vec![hash]).await?, @@ -275,8 +281,8 @@ impl Client { } /// Returns the bytes of a file from hydrus + #[tracing::instrument(skip(self), level = "debug")] pub async fn get_file(&self, id: FileIdentifier) -> Result { - log::trace!("Getting file {:?}", id); let response = match id { FileIdentifier::ID(id) => { self.get::(&[("file_id", id)]) @@ -300,28 +306,31 @@ impl Client { } /// Returns all files associated with the given url - pub async fn get_url_files>(&self, url: S) -> Result { - log::trace!("Getting files for url {}", url.as_ref()); + #[tracing::instrument(skip(self), level = "debug")] + pub async fn get_url_files + Debug>( + &self, + url: S, + ) -> Result { self.get_and_parse::(&[("url", url.as_ref())]) .await } /// Returns information about the given url - pub async fn get_url_info>(&self, url: S) -> Result { - log::trace!("Getting info for url {}", url.as_ref()); + #[tracing::instrument(skip(self), level = "debug")] + pub async fn get_url_info + Debug>(&self, url: S) -> Result { self.get_and_parse::(&[("url", url.as_ref())]) .await } /// Adds an url to hydrus, optionally with additional tags and a destination page + #[tracing::instrument(skip(self), level = "debug")] pub async fn add_url(&self, request: AddUrlRequest) -> Result { - log::trace!("Adding url {:?}", request); self.post_and_parse::(request).await } /// Associates urls with the given file hashes + #[tracing::instrument(skip(self), level = "debug")] pub async fn associate_urls(&self, urls: Vec, hashes: Vec) -> Result<()> { - log::trace!("Associating urls {:?} with hashes {:?}", urls, hashes); self.post::(AssociateUrlRequest { hashes, urls_to_add: urls, @@ -333,8 +342,8 @@ impl Client { } /// Disassociates urls with the given file hashes + #[tracing::instrument(skip(self), level = "debug")] pub async fn disassociate_urls(&self, urls: Vec, hashes: Vec) -> Result<()> { - log::trace!("Disassociating urls {:?} with hashes {:?}", urls, hashes); self.post::(AssociateUrlRequest { hashes, urls_to_add: vec![], @@ -346,22 +355,25 @@ impl Client { } /// Returns all pages of the client + #[tracing::instrument(skip(self), level = "debug")] pub async fn get_pages(&self) -> Result { - log::trace!("Getting pages"); self.get_and_parse::(&()).await } /// Returns information about a single page - pub async fn get_page_info>(&self, page_key: S) -> Result { - log::trace!("Getting information for page {}", page_key.as_ref()); + #[tracing::instrument(skip(self), level = "debug")] + pub async fn get_page_info + Debug>( + &self, + page_key: S, + ) -> Result { self.get_and_parse::(&[("page_key", page_key.as_ref())]) .await } /// Focuses a page in the client - pub async fn focus_page(&self, page_key: S) -> Result<()> { + #[tracing::instrument(skip(self), level = "debug")] + pub async fn focus_page(&self, page_key: S) -> Result<()> { let page_key = page_key.to_string(); - log::trace!("Focussing page {}", page_key); self.post::(FocusPageRequest { page_key }) .await?; @@ -369,19 +381,14 @@ impl Client { } /// Adds files to a page - pub async fn add_files_to_page( + #[tracing::instrument(skip(self), level = "debug")] + pub async fn add_files_to_page( &self, page_key: S, file_ids: Vec, hashes: Vec, ) -> Result<()> { let page_key = page_key.to_string(); - log::trace!( - "Adding files with ids {:?} or hashes {:?} to page {}", - file_ids, - hashes, - page_key - ); self.post::(AddFilesRequest { page_key, file_ids, @@ -393,8 +400,11 @@ impl Client { } /// Returns all cookies for the given domain - pub async fn get_cookies>(&self, domain: S) -> Result { - log::trace!("Getting cookies"); + #[tracing::instrument(skip(self), level = "debug")] + pub async fn get_cookies + Debug>( + &self, + domain: S, + ) -> Result { self.get_and_parse::(&[("domain", domain.as_ref())]) .await } @@ -402,8 +412,8 @@ impl Client { /// Sets some cookies for some websites. /// Each entry needs to be in the format `[, , , , ]` /// with the types `[String, String, String, String, u64]` + #[tracing::instrument(skip(self), level = "debug")] pub async fn set_cookies(&self, cookies: Vec<[OptionalStringNumber; 5]>) -> Result<()> { - log::trace!("Setting cookies {:?}", cookies); self.post::(SetCookiesRequest { cookies }) .await?; @@ -411,9 +421,9 @@ impl Client { } /// Sets the user agent that is being used for every request hydrus starts - pub async fn set_user_agent(&self, user_agent: S) -> Result<()> { + #[tracing::instrument(skip(self), level = "debug")] + pub async fn set_user_agent(&self, user_agent: S) -> Result<()> { let user_agent = user_agent.to_string(); - log::trace!("Setting user agent to {}", user_agent); self.post::(SetUserAgentRequest { user_agent }) .await?; diff --git a/src/api_core/mod.rs b/src/api_core/mod.rs index 9f87869..6058fcd 100644 --- a/src/api_core/mod.rs +++ b/src/api_core/mod.rs @@ -1,5 +1,6 @@ use serde::de::DeserializeOwned; use serde::Serialize; +use std::fmt::Debug; pub mod access_management; pub mod adding_files; @@ -13,8 +14,8 @@ pub mod searching_and_fetching_files; pub use searching_and_fetching_files::file_sort_type; pub(crate) trait Endpoint { - type Request: Serialize; - type Response: DeserializeOwned; + type Request: Serialize + Debug; + type Response: DeserializeOwned + Debug; fn path() -> String; } diff --git a/src/wrapper/hydrus.rs b/src/wrapper/hydrus.rs index 031ee28..13dbeae 100644 --- a/src/wrapper/hydrus.rs +++ b/src/wrapper/hydrus.rs @@ -10,6 +10,7 @@ use crate::wrapper::service::Services; use crate::wrapper::url::Url; use crate::wrapper::version::Version; use crate::Client; +use std::fmt::Debug; /// A high level wrapper for the hydrus API for easier management of files, tags /// urls etc. @@ -47,13 +48,13 @@ impl Hydrus { } /// Returns the address as an object that can be used to get and set cookies - pub fn address>(&self, address: S) -> Address { + pub fn address + Debug>(&self, address: S) -> Address { Address::from_str(self.client.clone(), address.as_ref()) } /// Returns information about a given url in an object that allows /// further operations with that url - pub async fn url>(&self, url: S) -> Result { + pub async fn url + Debug>(&self, url: S) -> Result { let info = self.client.get_url_info(&url).await?; Ok(Url { @@ -87,7 +88,7 @@ impl Hydrus { } /// Returns a hydrus page by page key - pub async fn page>(&self, page_key: S) -> Result { + pub async fn page + Debug>(&self, page_key: S) -> Result { let info_response = self.client.get_page_info(page_key).await?; Ok(HydrusPage::from_info( @@ -107,7 +108,7 @@ impl Hydrus { } /// Sets the user agent hydrus uses for http requests - pub async fn set_user_agent(&self, user_agent: S) -> Result<()> { + pub async fn set_user_agent(&self, user_agent: S) -> Result<()> { self.client.set_user_agent(user_agent).await } } diff --git a/src/wrapper/or_chain.rs b/src/wrapper/or_chain.rs index 7afacf4..3f0b022 100644 --- a/src/wrapper/or_chain.rs +++ b/src/wrapper/or_chain.rs @@ -47,7 +47,7 @@ where }) .map(Tag::from) .collect(); - log::debug!("String parsed to or-chain {:?}", tags); + tracing::debug!("String parsed to or-chain {:?}", tags); Self { tags } } diff --git a/tests/common.rs b/tests/common.rs index 576bfef..5dda720 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,6 +1,5 @@ use hydrus_api::api_core::client::Client; use hydrus_api::Hydrus; -use log::LevelFilter; use std::env; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -9,9 +8,7 @@ pub fn setup() { lazy_static::lazy_static! { static ref SETUP_DONE: Arc = Arc::new(AtomicBool::new(false)); } if !SETUP_DONE.swap(true, Ordering::SeqCst) { dotenv::dotenv().expect("failed to initialize dotenv"); - env_logger::builder() - .filter_level(LevelFilter::Trace) - .init(); + tracing_subscriber::fmt::init(); } } From be51ac405476f9d1f6b66337c2e280d77a0c7fc0 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 12:10:06 +0100 Subject: [PATCH 03/11] Fix automated tests Signed-off-by: trivernis --- Cargo.toml | 8 ++++---- tests/client/test_searching_and_fetching_files.rs | 2 +- tests/common.rs | 10 ++++++---- tests/wrapper/test_files.rs | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 089e0ab..b0a2ddc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,8 @@ repository = "https://github.com/trivernis/hydrus-api-rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = {version = "^1.0", features = ["derive"]} -reqwest = {version = "0.11.4", features = ["json"]} +serde = { version = "1.0.136", features = ["derive"] } +reqwest = { version = "0.11.9", features = ["json"] } tracing = "0.1.31" mime = "0.3.16" chrono = "0.4.19" @@ -25,8 +25,8 @@ dotenv = "0.15.0" tracing-subscriber = "0.3.9" [dev-dependencies.tokio] -version = "1.8.0" +version = "1.17.0" features = ["macros", "rt-multi-thread"] [features] -rustls = ["reqwest/rustls"] \ No newline at end of file +rustls = ["reqwest/rustls"] diff --git a/tests/client/test_searching_and_fetching_files.rs b/tests/client/test_searching_and_fetching_files.rs index 89dcd14..0a1eed0 100644 --- a/tests/client/test_searching_and_fetching_files.rs +++ b/tests/client/test_searching_and_fetching_files.rs @@ -8,7 +8,7 @@ async fn is_searches_files() { let client = common::get_client(); let options = FileSearchOptions::new() .sort_type(SORT_FILE_PIXEL_COUNT) - .tag_service_name("public tag repository") + .tag_service_name("my tags") .file_service_name("all known files"); client .search_files( diff --git a/tests/common.rs b/tests/common.rs index 5dda720..78cdac3 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,14 +1,16 @@ use hydrus_api::api_core::client::Client; use hydrus_api::Hydrus; use std::env; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Mutex, MutexGuard}; pub fn setup() { - lazy_static::lazy_static! { static ref SETUP_DONE: Arc = Arc::new(AtomicBool::new(false)); } - if !SETUP_DONE.swap(true, Ordering::SeqCst) { + lazy_static::lazy_static! { static ref SETUP_DONE: Arc> = Arc::new(Mutex::new(false)); } + let mut setup_done: MutexGuard = SETUP_DONE.lock().unwrap(); + + if !*setup_done { dotenv::dotenv().expect("failed to initialize dotenv"); tracing_subscriber::fmt::init(); + *setup_done = true; } } diff --git a/tests/wrapper/test_files.rs b/tests/wrapper/test_files.rs index 5291bfe..1e40692 100644 --- a/tests/wrapper/test_files.rs +++ b/tests/wrapper/test_files.rs @@ -54,7 +54,7 @@ async fn it_has_tags() { async fn it_adds_tags() { let mut file = get_file().await; file.add_tags( - ServiceName::public_tag_repository(), + ServiceName::my_tags(), vec!["character:megumin".into(), "ark mage".into()], ) .await @@ -65,7 +65,7 @@ async fn it_adds_tags() { async fn it_modifies_tags() { let mut file = get_file().await; file.modify_tags( - ServiceName::public_tag_repository(), + ServiceName::my_tags(), TagAction::RescindPendFromRepository, vec!["ark mage".into()], ) From 7260eb825ad982c735ae35e96341cda890e32cb9 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 12:27:43 +0100 Subject: [PATCH 04/11] Add support for adding tags to services by service keys Signed-off-by: trivernis --- src/api_core/adding_tags.rs | 58 +++++++++++++------------ src/api_core/client.rs | 1 + src/api_core/common.rs | 16 +++++++ src/wrapper/builders/tagging_builder.rs | 15 ++++--- src/wrapper/hydrus_file.rs | 9 ++-- src/wrapper/service.rs | 7 +++ tests/client/test_adding_tags.rs | 12 ++++- tests/wrapper/test_hydrus.rs | 2 +- 8 files changed, 82 insertions(+), 38 deletions(-) diff --git a/src/api_core/adding_tags.rs b/src/api_core/adding_tags.rs index 91c883f..1c9aafe 100644 --- a/src/api_core/adding_tags.rs +++ b/src/api_core/adding_tags.rs @@ -1,3 +1,4 @@ +use crate::api_core::common::ServiceIdentifier; use crate::api_core::Endpoint; use std::collections::HashMap; @@ -21,7 +22,9 @@ impl Endpoint for CleanTags { pub struct AddTagsRequest { pub hashes: Vec, pub service_names_to_tags: HashMap>, + pub service_keys_to_tags: HashMap>, pub service_names_to_actions_to_tags: HashMap>>, + pub service_keys_to_actions_to_tags: HashMap>>, } pub struct AddTags; @@ -35,10 +38,13 @@ impl Endpoint for AddTags { } } +#[derive(Default)] pub struct AddTagsRequestBuilder { hashes: Vec, service_names_to_tags: HashMap>, + service_keys_to_tags: HashMap>, service_names_to_actions_to_tags: HashMap>>, + service_keys_to_actions_to_tags: HashMap>>, } /// List of actions for a given tag @@ -78,16 +84,6 @@ impl TagAction { } } -impl Default for AddTagsRequestBuilder { - fn default() -> Self { - Self { - hashes: vec![], - service_names_to_tags: Default::default(), - service_names_to_actions_to_tags: Default::default(), - } - } -} - impl AddTagsRequestBuilder { /// Adds a file hash to the request pub fn add_hash>(mut self, hash: S) -> Self { @@ -104,41 +100,48 @@ impl AddTagsRequestBuilder { } /// Adds a single tag for a given service - pub fn add_tag, S2: AsRef>(mut self, service_name: S1, tag: S2) -> Self { - if let Some(mappings) = self.service_names_to_tags.get_mut(service_name.as_ref()) { + pub fn add_tag>(mut self, service_id: ServiceIdentifier, tag: S) -> Self { + let (service, relevant_mappings) = match service_id { + ServiceIdentifier::Name(name) => (name, &mut self.service_names_to_tags), + ServiceIdentifier::Key(key) => (key, &mut self.service_keys_to_tags), + }; + if let Some(mappings) = relevant_mappings.get_mut(&service) { mappings.push(tag.as_ref().into()) } else { - self.service_names_to_tags - .insert(service_name.as_ref().into(), vec![tag.as_ref().into()]); + relevant_mappings.insert(service, vec![tag.as_ref().into()]); } self } /// Adds multiple tags for a given service - pub fn add_tags>(mut self, service_name: S1, mut tags: Vec) -> Self { - if let Some(mappings) = self.service_names_to_tags.get_mut(service_name.as_ref()) { + pub fn add_tags(mut self, service_id: ServiceIdentifier, mut tags: Vec) -> Self { + let (service, relevant_mappings) = match service_id { + ServiceIdentifier::Name(name) => (name, &mut self.service_names_to_tags), + ServiceIdentifier::Key(key) => (key, &mut self.service_keys_to_tags), + }; + if let Some(mappings) = relevant_mappings.get_mut(&service) { mappings.append(&mut tags); } else { - self.service_names_to_tags - .insert(service_name.as_ref().into(), tags); + relevant_mappings.insert(service, tags); } self } /// Adds one tag for a given service with a defined action - pub fn add_tag_with_action, S2: AsRef>( + pub fn add_tag_with_action>( mut self, - service_name: S1, - tag: S2, + service_id: ServiceIdentifier, + tag: S, action: TagAction, ) -> Self { + let (service, relevant_mappings) = match service_id { + ServiceIdentifier::Name(name) => (name, &mut self.service_names_to_actions_to_tags), + ServiceIdentifier::Key(key) => (key, &mut self.service_keys_to_actions_to_tags), + }; let action_id = action.into_id(); - if let Some(actions) = self - .service_names_to_actions_to_tags - .get_mut(service_name.as_ref()) - { + if let Some(actions) = relevant_mappings.get_mut(&service) { if let Some(tags) = actions.get_mut(&action_id.to_string()) { tags.push(tag.as_ref().into()); } else { @@ -147,8 +150,7 @@ impl AddTagsRequestBuilder { } else { let mut actions = HashMap::new(); actions.insert(action_id.to_string(), vec![tag.as_ref().into()]); - self.service_names_to_actions_to_tags - .insert(service_name.as_ref().into(), actions); + relevant_mappings.insert(service, actions); } self } @@ -158,7 +160,9 @@ impl AddTagsRequestBuilder { AddTagsRequest { hashes: self.hashes, service_names_to_tags: self.service_names_to_tags, + service_keys_to_tags: self.service_keys_to_tags, service_names_to_actions_to_tags: self.service_names_to_actions_to_tags, + service_keys_to_actions_to_tags: self.service_keys_to_actions_to_tags, } } } diff --git a/src/api_core/client.rs b/src/api_core/client.rs index 7905356..44a3662 100644 --- a/src/api_core/client.rs +++ b/src/api_core/client.rs @@ -228,6 +228,7 @@ impl Client { } /// Adds tags to files with the given hashes + #[tracing::instrument(skip(self), level = "debug")] pub async fn add_tags(&self, request: AddTagsRequest) -> Result<()> { self.post::(request).await?; diff --git a/src/api_core/common.rs b/src/api_core/common.rs index a23f9cb..26809e1 100644 --- a/src/api_core/common.rs +++ b/src/api_core/common.rs @@ -6,6 +6,22 @@ pub struct BasicServiceInfo { pub service_key: String, } +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialOrd, PartialEq, Ord, Eq)] +pub enum ServiceIdentifier { + Name(String), + Key(String), +} + +impl ServiceIdentifier { + pub fn name(name: S) -> Self { + Self::Name(name.to_string()) + } + + pub fn key(key: S) -> Self { + Self::Key(key.to_string()) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BasicHashList { pub hashes: Vec, diff --git a/src/wrapper/builders/tagging_builder.rs b/src/wrapper/builders/tagging_builder.rs index c80fa8b..07f85b0 100644 --- a/src/wrapper/builders/tagging_builder.rs +++ b/src/wrapper/builders/tagging_builder.rs @@ -1,6 +1,6 @@ use crate::api_core::adding_tags::{AddTagsRequestBuilder, TagAction}; +use crate::api_core::common::ServiceIdentifier; use crate::error::Result; -use crate::wrapper::service::ServiceName; use crate::wrapper::tag::Tag; use crate::Client; use std::collections::HashMap; @@ -8,7 +8,7 @@ use std::collections::HashMap; pub struct TaggingBuilder { client: Client, hashes: Vec, - tag_mappings: HashMap>>, + tag_mappings: HashMap>>, } impl TaggingBuilder { @@ -28,12 +28,17 @@ impl TaggingBuilder { } /// Adds a single tag for a given service - pub fn add_tag(self, service: ServiceName, action: TagAction, tag: Tag) -> Self { + pub fn add_tag(self, service: ServiceIdentifier, action: TagAction, tag: Tag) -> Self { self.add_tags(service, action, vec![tag]) } /// Adds tags with actions for the given service - pub fn add_tags(mut self, service: ServiceName, action: TagAction, mut tags: Vec) -> Self { + pub fn add_tags( + mut self, + service: ServiceIdentifier, + action: TagAction, + mut tags: Vec, + ) -> Self { let service_action_mappings = if let Some(service_action_mappings) = self.tag_mappings.get_mut(&service) { service_action_mappings @@ -57,7 +62,7 @@ impl TaggingBuilder { for (action, tags) in action_tag_mappings { for tag in tags { request = request.add_tag_with_action( - service.0.clone(), + service.clone().into(), tag.to_string(), action.clone(), ); diff --git a/src/wrapper/hydrus_file.rs b/src/wrapper/hydrus_file.rs index a0312ea..d079078 100644 --- a/src/wrapper/hydrus_file.rs +++ b/src/wrapper/hydrus_file.rs @@ -217,7 +217,7 @@ impl HydrusFile { let hash = self.hash().await?; let request = AddTagsRequestBuilder::default() .add_hash(hash) - .add_tags(service.0, tag_list_to_string_list(tags)) + .add_tags(service.into(), tag_list_to_string_list(tags)) .build(); self.client.add_tags(request).await @@ -234,8 +234,11 @@ impl HydrusFile { let mut reqwest = AddTagsRequestBuilder::default().add_hash(hash); for tag in tags { - reqwest = - reqwest.add_tag_with_action(service.0.clone(), tag.to_string(), action.clone()); + reqwest = reqwest.add_tag_with_action( + service.clone().into(), + tag.to_string(), + action.clone(), + ); } self.client.add_tags(reqwest.build()).await diff --git a/src/wrapper/service.rs b/src/wrapper/service.rs index f85998e..c8b9ab1 100644 --- a/src/wrapper/service.rs +++ b/src/wrapper/service.rs @@ -5,6 +5,7 @@ use crate::api_core::access_management::{ SERVICE_TYPE_TAG_REPOSITORIES, SERVICE_TYPE_TRASH, }; +use crate::api_core::common::ServiceIdentifier; use crate::error::Error; use crate::wrapper::builders::search_builder::SearchBuilder; use crate::Client; @@ -96,6 +97,12 @@ impl Display for ServiceName { } } +impl Into for ServiceName { + fn into(self) -> ServiceIdentifier { + ServiceIdentifier::Name(self.0) + } +} + #[derive(Clone)] pub struct Service { client: Client, diff --git a/tests/client/test_adding_tags.rs b/tests/client/test_adding_tags.rs index 2dacb13..467d08f 100644 --- a/tests/client/test_adding_tags.rs +++ b/tests/client/test_adding_tags.rs @@ -1,5 +1,6 @@ use super::super::common; use hydrus_api::api_core::adding_tags::{AddTagsRequestBuilder, TagAction}; +use hydrus_api::api_core::common::ServiceIdentifier; #[tokio::test] async fn it_cleans_tags() { @@ -21,8 +22,15 @@ async fn it_adds_tags() { let client = common::get_client(); let request = AddTagsRequestBuilder::default() .add_hash("0000000000000000000000000000000000000000000000000000000000000000") // valid hash, I hope no files are affected - .add_tags("my tags", vec!["beach".into(), "summer".into()]) - .add_tag_with_action("my tags", "rain", TagAction::DeleteFromLocalService) + .add_tags( + ServiceIdentifier::name("my tags"), + vec!["beach".into(), "summer".into()], + ) + .add_tag_with_action( + ServiceIdentifier::name("my tags"), + "rain", + TagAction::DeleteFromLocalService, + ) .build(); client.add_tags(request).await.unwrap(); } diff --git a/tests/wrapper/test_hydrus.rs b/tests/wrapper/test_hydrus.rs index 7f10325..777d1d3 100644 --- a/tests/wrapper/test_hydrus.rs +++ b/tests/wrapper/test_hydrus.rs @@ -60,7 +60,7 @@ async fn it_adds_tags() { hydrus .tagging() .add_tag( - ServiceName::my_tags(), + ServiceName::my_tags().into(), TagAction::AddToLocalService, "summer".into(), ) From b52c28e31e92159d5c5275f3942d0ad53da7b695 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 13:36:25 +0100 Subject: [PATCH 05/11] Improve tests and wrapper functions Signed-off-by: trivernis --- src/api_core/adding_urls.rs | 41 +++++++------------ src/api_core/client.rs | 12 ++++-- src/api_core/client_builder.rs | 56 ++++++++++++++++++++++++++ src/api_core/common.rs | 8 ++++ src/api_core/mod.rs | 2 + src/error.rs | 2 + src/wrapper/builders/import_builder.rs | 6 ++- src/wrapper/hydrus_file.rs | 14 +++---- tests/client/test_adding_urls.rs | 3 +- tests/common.rs | 12 +++--- tests/wrapper/test_files.rs | 4 +- tests/wrapper/test_tags.rs | 4 ++ 12 files changed, 117 insertions(+), 47 deletions(-) create mode 100644 src/api_core/client_builder.rs diff --git a/src/api_core/adding_urls.rs b/src/api_core/adding_urls.rs index a92aabc..33aae0b 100644 --- a/src/api_core/adding_urls.rs +++ b/src/api_core/adding_urls.rs @@ -1,3 +1,4 @@ +use crate::api_core::common::ServiceIdentifier; use crate::api_core::Endpoint; use serde::Serialize; use std::collections::HashMap; @@ -52,7 +53,7 @@ impl Endpoint for GetUrlInfo { } } -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Default, Debug, Serialize)] pub struct AddUrlRequest { pub url: String, @@ -64,6 +65,7 @@ pub struct AddUrlRequest { pub show_destination_page: bool, pub service_names_to_additional_tags: HashMap>, + pub service_keys_to_additional_tags: HashMap>, pub filterable_tags: Vec, } @@ -73,33 +75,20 @@ pub struct AddUrlRequest { /// Example: /// ``` /// use hydrus_api::api_core::adding_urls::AddUrlRequestBuilder; +/// use hydrus_api::api_core::common::ServiceIdentifier; /// /// let request = AddUrlRequestBuilder::default() /// .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") -/// .add_tags("my tags", vec!["ark mage".to_string(), "grinning".to_string()]) +/// .add_tags(ServiceIdentifier::name("my tags"), vec!["ark mage".to_string(), "grinning".to_string()]) /// .show_destination_page(true) /// .destination_page_name("Rusty Url Import") /// .build(); /// ``` +#[derive(Default)] pub struct AddUrlRequestBuilder { inner: AddUrlRequest, } -impl Default for AddUrlRequestBuilder { - fn default() -> Self { - Self { - inner: AddUrlRequest { - url: String::new(), - destination_page_key: None, - destination_page_name: None, - show_destination_page: false, - service_names_to_additional_tags: Default::default(), - filterable_tags: vec![], - }, - } - } -} - impl AddUrlRequestBuilder { pub fn url(mut self, url: S) -> Self { self.inner.url = url.to_string(); @@ -125,17 +114,17 @@ impl AddUrlRequestBuilder { self } - pub fn add_tags>(mut self, service: S, mut tags: Vec) -> Self { - if let Some(entry) = self - .inner - .service_names_to_additional_tags - .get_mut(service.as_ref()) - { + pub fn add_tags(mut self, service_id: ServiceIdentifier, mut tags: Vec) -> Self { + let (service, mappings) = match service_id { + ServiceIdentifier::Name(name) => { + (name, &mut self.inner.service_names_to_additional_tags) + } + ServiceIdentifier::Key(key) => (key, &mut self.inner.service_keys_to_additional_tags), + }; + if let Some(entry) = mappings.get_mut(&service) { entry.append(&mut tags); } else { - self.inner - .service_names_to_additional_tags - .insert(service.as_ref().to_string(), tags); + mappings.insert(service, tags); } self diff --git a/src/api_core/client.rs b/src/api_core/client.rs index 44a3662..cab5721 100644 --- a/src/api_core/client.rs +++ b/src/api_core/client.rs @@ -11,6 +11,7 @@ use crate::api_core::adding_urls::{ AddUrl, AddUrlRequest, AddUrlResponse, AssociateUrl, AssociateUrlRequest, GetUrlFiles, GetUrlFilesResponse, GetUrlInfo, GetUrlInfoResponse, }; +use crate::api_core::client_builder::ClientBuilder; use crate::api_core::common::{FileIdentifier, FileMetadataInfo, FileRecord, OptionalStringNumber}; use crate::api_core::managing_cookies_and_http_headers::{ GetCookies, GetCookiesResponse, SetCookies, SetCookiesRequest, SetUserAgent, @@ -41,12 +42,17 @@ static ACCESS_KEY_HEADER: &str = "Hydrus-Client-API-Access-Key"; /// over the REST api. #[derive(Debug)] pub struct Client { - inner: reqwest::Client, - base_url: String, - access_key: String, + pub(crate) inner: reqwest::Client, + pub(crate) base_url: String, + pub(crate) access_key: String, } impl Client { + /// Returns a builder for the client + pub fn builder() -> ClientBuilder { + ClientBuilder::default() + } + /// Creates a new client to start requests against the hydrus api. pub fn new>(url: S, access_key: S) -> Self { Self { diff --git a/src/api_core/client_builder.rs b/src/api_core/client_builder.rs new file mode 100644 index 0000000..2ae0112 --- /dev/null +++ b/src/api_core/client_builder.rs @@ -0,0 +1,56 @@ +use crate::error::{Error, Result}; +use crate::Client; +use std::time::Duration; + +pub struct ClientBuilder { + reqwest_builder: reqwest::ClientBuilder, + base_url: String, + access_key: Option, +} + +impl Default for ClientBuilder { + fn default() -> Self { + Self { + reqwest_builder: Default::default(), + base_url: "127.0.0.1:45869".to_string(), + access_key: None, + } + } +} + +impl ClientBuilder { + /// Set the base url with port for the client api + /// The default value is `127.0.0.1:45869` + pub fn url(mut self, url: S) -> Self { + self.base_url = url.to_string(); + + self + } + + /// Sets the access key for the client. + /// The key is required + pub fn access_key(mut self, key: S) -> Self { + self.access_key = Some(key.to_string()); + + self + } + + /// Sets the default timeout for requests to the API + pub fn timeout(mut self, timeout: Duration) -> Self { + self.reqwest_builder = self.reqwest_builder.timeout(timeout); + + self + } + + /// Builds the client + pub fn build(self) -> Result { + let access_key = self + .access_key + .ok_or_else(|| Error::BuildError(String::from("missing access key")))?; + Ok(Client { + inner: self.reqwest_builder.build()?, + base_url: self.base_url, + access_key, + }) + } +} diff --git a/src/api_core/common.rs b/src/api_core/common.rs index 26809e1..9fc631a 100644 --- a/src/api_core/common.rs +++ b/src/api_core/common.rs @@ -6,6 +6,14 @@ pub struct BasicServiceInfo { pub service_key: String, } +impl BasicServiceInfo { + /// Converts the Service into into an identifier + /// that can be used for requests consuming service references + pub fn into_id(self) -> ServiceIdentifier { + ServiceIdentifier::Key(self.service_key) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialOrd, PartialEq, Ord, Eq)] pub enum ServiceIdentifier { Name(String), diff --git a/src/api_core/mod.rs b/src/api_core/mod.rs index 6058fcd..bf5b68c 100644 --- a/src/api_core/mod.rs +++ b/src/api_core/mod.rs @@ -7,10 +7,12 @@ pub mod adding_files; pub mod adding_tags; pub mod adding_urls; pub mod client; +pub mod client_builder; pub mod common; pub mod managing_cookies_and_http_headers; pub mod managing_pages; pub mod searching_and_fetching_files; + pub use searching_and_fetching_files::file_sort_type; pub(crate) trait Endpoint { diff --git a/src/error.rs b/src/error.rs index 60f3ad6..7ac5198 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,7 @@ pub enum Error { ImportFailed(String), FileNotFound(FileIdentifier), InvalidMime(String), + BuildError(String), } impl fmt::Display for Error { @@ -27,6 +28,7 @@ impl fmt::Display for Error { Self::ImportVetoed(msg) => write!(f, "File import vetoed: {}", msg), Self::FileNotFound(id) => write!(f, "File {:?} not found", id), Self::InvalidMime(mime) => write!(f, "Failed to parse invalid mime {}", mime), + Self::BuildError(error) => write!(f, "Build error {}", error), } } } diff --git a/src/wrapper/builders/import_builder.rs b/src/wrapper/builders/import_builder.rs index f9b9e6d..a99b2aa 100644 --- a/src/wrapper/builders/import_builder.rs +++ b/src/wrapper/builders/import_builder.rs @@ -1,5 +1,6 @@ use crate::api_core::adding_files::{STATUS_IMPORT_FAILED, STATUS_IMPORT_VETOED}; use crate::api_core::adding_urls::AddUrlRequestBuilder; +use crate::api_core::common::ServiceIdentifier; use crate::error::{Error, Result}; use crate::utils::tag_list_to_string_list; use crate::wrapper::hydrus_file::HydrusFile; @@ -141,7 +142,10 @@ impl UrlImportBuilder { let mut request = AddUrlRequestBuilder::default().url(&self.url); for (service, tags) in self.service_tag_mappings { - request = request.add_tags(service, tag_list_to_string_list(tags)); + request = request.add_tags( + ServiceIdentifier::name(service), + tag_list_to_string_list(tags), + ); } request = request.add_filter_tags(tag_list_to_string_list(self.filter_tags)); if let Some(page) = self.page { diff --git a/src/wrapper/hydrus_file.rs b/src/wrapper/hydrus_file.rs index d079078..fd9c8ca 100644 --- a/src/wrapper/hydrus_file.rs +++ b/src/wrapper/hydrus_file.rs @@ -1,5 +1,5 @@ use crate::api_core::adding_tags::{AddTagsRequestBuilder, TagAction}; -use crate::api_core::common::{FileIdentifier, FileMetadataInfo, FileRecord}; +use crate::api_core::common::{FileIdentifier, FileMetadataInfo, FileRecord, ServiceIdentifier}; use crate::error::{Error, Result}; use crate::utils::tag_list_to_string_list; use crate::wrapper::service::ServiceName; @@ -213,11 +213,11 @@ impl HydrusFile { } /// Adds tags for a specific service to the file - pub async fn add_tags(&mut self, service: ServiceName, tags: Vec) -> Result<()> { + pub async fn add_tags(&mut self, service: ServiceIdentifier, tags: Vec) -> Result<()> { let hash = self.hash().await?; let request = AddTagsRequestBuilder::default() .add_hash(hash) - .add_tags(service.into(), tag_list_to_string_list(tags)) + .add_tags(service, tag_list_to_string_list(tags)) .build(); self.client.add_tags(request).await @@ -226,7 +226,7 @@ impl HydrusFile { /// Allows modification of tags by using the defined tag actions pub async fn modify_tags( &mut self, - service: ServiceName, + service: ServiceIdentifier, action: TagAction, tags: Vec, ) -> Result<()> { @@ -234,11 +234,7 @@ impl HydrusFile { let mut reqwest = AddTagsRequestBuilder::default().add_hash(hash); for tag in tags { - reqwest = reqwest.add_tag_with_action( - service.clone().into(), - tag.to_string(), - action.clone(), - ); + reqwest = reqwest.add_tag_with_action(service.clone(), tag.to_string(), action.clone()); } self.client.add_tags(reqwest.build()).await diff --git a/tests/client/test_adding_urls.rs b/tests/client/test_adding_urls.rs index 8b1f9aa..b262f9d 100644 --- a/tests/client/test_adding_urls.rs +++ b/tests/client/test_adding_urls.rs @@ -1,5 +1,6 @@ use super::super::common; use hydrus_api::api_core::adding_urls::{AddUrlRequestBuilder, URL_TYPE_POST}; +use hydrus_api::api_core::common::ServiceIdentifier; #[tokio::test] async fn it_returns_files_for_an_url() { @@ -29,7 +30,7 @@ async fn it_adds_urls() { let request = AddUrlRequestBuilder::default() .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") .add_tags( - "my tags", + ServiceIdentifier::name("my tags"), vec!["ark mage".to_string(), "grinning".to_string()], ) .show_destination_page(true) diff --git a/tests/common.rs b/tests/common.rs index 78cdac3..996e6b5 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -2,6 +2,7 @@ use hydrus_api::api_core::client::Client; use hydrus_api::Hydrus; use std::env; use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::Duration; pub fn setup() { lazy_static::lazy_static! { static ref SETUP_DONE: Arc> = Arc::new(Mutex::new(false)); } @@ -16,11 +17,12 @@ pub fn setup() { pub fn get_client() -> Client { setup(); - - Client::new( - env::var("HYDRUS_URL").unwrap(), - env::var("HYDRUS_ACCESS_KEY").unwrap(), - ) + Client::builder() + .url(env::var("HYDRUS_URL").unwrap()) + .access_key(env::var("HYDRUS_ACCESS_KEY").unwrap()) + .timeout(Duration::from_secs(5)) + .build() + .unwrap() } pub fn get_hydrus() -> Hydrus { diff --git a/tests/wrapper/test_files.rs b/tests/wrapper/test_files.rs index 1e40692..b4a7947 100644 --- a/tests/wrapper/test_files.rs +++ b/tests/wrapper/test_files.rs @@ -54,7 +54,7 @@ async fn it_has_tags() { async fn it_adds_tags() { let mut file = get_file().await; file.add_tags( - ServiceName::my_tags(), + ServiceName::my_tags().into(), vec!["character:megumin".into(), "ark mage".into()], ) .await @@ -65,7 +65,7 @@ async fn it_adds_tags() { async fn it_modifies_tags() { let mut file = get_file().await; file.modify_tags( - ServiceName::my_tags(), + ServiceName::my_tags().into(), TagAction::RescindPendFromRepository, vec!["ark mage".into()], ) diff --git a/tests/wrapper/test_tags.rs b/tests/wrapper/test_tags.rs index 5d0580c..ba3349f 100644 --- a/tests/wrapper/test_tags.rs +++ b/tests/wrapper/test_tags.rs @@ -7,8 +7,12 @@ use hydrus_api::wrapper::builders::tag_builder::{ }; use hydrus_api::wrapper::service::ServiceName; use hydrus_api::wrapper::tag::Tag; +use std::sync::Arc; +use tokio::sync::Mutex; async fn retrieve_single_tag(tag: Tag) -> Result<()> { + lazy_static::lazy_static! { static ref SEM: Arc> = Arc::new(Mutex::new(())); } + SEM.lock().await; let hydrus = common::get_hydrus(); hydrus.search().add_tag(tag).run().await?; From 2b5599b821da2e415947387c92b0d9c130d4e8c9 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 13:49:24 +0100 Subject: [PATCH 06/11] Add support for returning file hashes on search Signed-off-by: trivernis --- src/api_core/client.rs | 20 ++++++++++++++++--- src/api_core/searching_and_fetching_files.rs | 16 +++++++++++++++ .../test_searching_and_fetching_files.rs | 19 ++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/api_core/client.rs b/src/api_core/client.rs index cab5721..5c65472 100644 --- a/src/api_core/client.rs +++ b/src/api_core/client.rs @@ -22,8 +22,8 @@ use crate::api_core::managing_pages::{ GetPages, GetPagesResponse, }; use crate::api_core::searching_and_fetching_files::{ - FileMetadata, FileMetadataResponse, FileSearchOptions, GetFile, SearchFiles, - SearchFilesResponse, SearchQueryEntry, + FileMetadata, FileMetadataResponse, FileSearchOptions, GetFile, SearchFileHashes, + SearchFileHashesResponse, SearchFiles, SearchFilesResponse, SearchQueryEntry, }; use crate::api_core::Endpoint; use crate::error::{Error, Result}; @@ -241,7 +241,7 @@ impl Client { Ok(()) } - /// Searches for files in the inbox, the archive or both + /// Searches for files #[tracing::instrument(skip(self), level = "debug")] pub async fn search_files( &self, @@ -254,6 +254,20 @@ impl Client { .await } + /// Searches for file hashes + #[tracing::instrument(skip(self), level = "debug")] + pub async fn search_file_hashes( + &self, + query: Vec, + options: FileSearchOptions, + ) -> Result { + let mut args = options.into_query_args(); + args.push(("tags", search_query_list_to_json_array(query))); + args.push(("return_hashes", String::from("true"))); + self.get_and_parse::(&args) + .await + } + /// Returns the metadata for a given list of file_ids or hashes #[tracing::instrument(skip(self), level = "debug")] pub async fn get_file_metadata( diff --git a/src/api_core/searching_and_fetching_files.rs b/src/api_core/searching_and_fetching_files.rs index a7a56e6..9899d99 100644 --- a/src/api_core/searching_and_fetching_files.rs +++ b/src/api_core/searching_and_fetching_files.rs @@ -112,6 +112,22 @@ impl Endpoint for SearchFiles { } } +#[derive(Clone, Debug, Deserialize)] +pub struct SearchFileHashesResponse { + pub hashes: Vec, +} + +pub struct SearchFileHashes; + +impl Endpoint for SearchFileHashes { + type Request = (); + type Response = SearchFileHashesResponse; + + fn path() -> String { + String::from("get_files/search_files") + } +} + #[derive(Clone, Debug, Default, Deserialize)] pub struct FileMetadataResponse { pub metadata: Vec, diff --git a/tests/client/test_searching_and_fetching_files.rs b/tests/client/test_searching_and_fetching_files.rs index 0a1eed0..7d42220 100644 --- a/tests/client/test_searching_and_fetching_files.rs +++ b/tests/client/test_searching_and_fetching_files.rs @@ -22,6 +22,25 @@ async fn is_searches_files() { .unwrap(); } +#[tokio::test] +async fn is_searches_file_hashes() { + let client = common::get_client(); + let options = FileSearchOptions::new() + .sort_type(SORT_FILE_PIXEL_COUNT) + .tag_service_name("my tags") + .file_service_name("all known files"); + client + .search_file_hashes( + vec![ + "beach".into(), + SearchQueryEntry::OrChain(vec!["summer".to_string(), "winter".to_string()]), + ], + options, + ) + .await + .unwrap(); +} + #[tokio::test] async fn it_fetches_file_metadata() { let client = common::get_client(); From b7f902e3dd0c5b8c000742e0dd67964feb5400c1 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 15:21:53 +0100 Subject: [PATCH 07/11] Add support for file service metadata timestamps Signed-off-by: trivernis --- src/api_core/common.rs | 20 +++++++ src/wrapper/hydrus_file.rs | 56 +++++++++++++++++++ .../test_searching_and_fetching_files.rs | 6 +- tests/wrapper/test_files.rs | 3 + 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/api_core/common.rs b/src/api_core/common.rs index 9fc631a..56668fa 100644 --- a/src/api_core/common.rs +++ b/src/api_core/common.rs @@ -45,6 +45,8 @@ pub struct FileMetadataInfo { pub width: Option, pub height: Option, pub duration: Option, + pub time_modified: Option, + pub file_services: FileMetadataServices, pub has_audio: Option, pub num_frames: Option, pub num_words: Option, @@ -54,6 +56,7 @@ pub struct FileMetadataInfo { pub known_urls: Vec, pub service_names_to_statuses_to_tags: HashMap>>, pub service_names_to_statuses_to_display_tags: HashMap>>, + pub service_keys_to_statuses_to_display_tags: HashMap>>, } #[derive(Clone, Debug)] @@ -74,6 +77,23 @@ pub struct FileRecord { pub mime_type: String, } +#[derive(Clone, Default, Debug, Deserialize)] +pub struct FileMetadataServices { + pub current: HashMap, + pub deleted: HashMap, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct FileMetadataServiceCurrent { + pub time_imported: u64, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct FileMetadataServiceDeleted { + pub time_deleted: u64, + pub time_imported: u64, +} + #[derive(Clone, Debug, Deserialize)] pub struct PageInformation { pub name: String, diff --git a/src/wrapper/hydrus_file.rs b/src/wrapper/hydrus_file.rs index fd9c8ca..1e612db 100644 --- a/src/wrapper/hydrus_file.rs +++ b/src/wrapper/hydrus_file.rs @@ -5,6 +5,7 @@ use crate::utils::tag_list_to_string_list; use crate::wrapper::service::ServiceName; use crate::wrapper::tag::Tag; use crate::Client; +use chrono::{NaiveDateTime, TimeZone, Utc}; use mime::Mime; use std::collections::HashMap; @@ -171,6 +172,61 @@ impl HydrusFile { Ok(metadata.is_trashed) } + /// Returns all urls associated with the file + pub async fn urls(&mut self) -> Result<&Vec> { + let metadata = self.metadata().await?; + + Ok(&metadata.known_urls) + } + + /// Returns the modified time of the file + pub async fn time_modified(&mut self) -> Result> { + let metadata = self.metadata().await?; + let naive_time_modified = metadata + .time_modified + .map(|m| Utc.timestamp_millis(m as i64).naive_utc()); + + Ok(naive_time_modified) + } + + /// Returns the imported time of the file for a given file service key + pub async fn time_imported>( + &mut self, + service_key: S, + ) -> Result> { + let metadata = self.metadata().await?; + let naive_time_imported = metadata + .file_services + .current + .get(service_key.as_ref()) + .map(|s| s.time_imported) + .or_else(|| { + metadata + .file_services + .deleted + .get(service_key.as_ref()) + .map(|s| s.time_imported) + }) + .map(|millis| Utc.timestamp_millis(millis as i64).naive_utc()); + + Ok(naive_time_imported) + } + + pub async fn time_deleted>( + &mut self, + service_key: S, + ) -> Result> { + let metadata = self.metadata().await?; + let naive_time_deleted = metadata + .file_services + .deleted + .get(service_key.as_ref()) + .map(|service| service.time_deleted) + .map(|millis| Utc.timestamp_millis(millis as i64).naive_utc()); + + Ok(naive_time_deleted) + } + /// Associates the file with a list of urls pub async fn associate_urls(&mut self, urls: Vec) -> Result<()> { let hash = self.hash().await?; diff --git a/tests/client/test_searching_and_fetching_files.rs b/tests/client/test_searching_and_fetching_files.rs index 7d42220..dde77ad 100644 --- a/tests/client/test_searching_and_fetching_files.rs +++ b/tests/client/test_searching_and_fetching_files.rs @@ -44,13 +44,13 @@ async fn is_searches_file_hashes() { #[tokio::test] async fn it_fetches_file_metadata() { let client = common::get_client(); - let response = client + client .get_file_metadata( vec![], vec!["0000000000000000000000000000000000000000000000000000000000000000".to_string()], ) - .await; - assert!(response.is_ok()); // Even if the file doesn't exist it still returns some information about it + .await + .unwrap(); } #[tokio::test] diff --git a/tests/wrapper/test_files.rs b/tests/wrapper/test_files.rs index b4a7947..f464526 100644 --- a/tests/wrapper/test_files.rs +++ b/tests/wrapper/test_files.rs @@ -87,4 +87,7 @@ async fn it_retrieves_metadata() { assert!(file.dimensions().await.unwrap().is_some()); assert!(file.stored_locally().await.unwrap()); assert!(file.duration().await.unwrap().is_none()); + assert!(file.time_modified().await.is_ok()); + assert!(file.time_deleted("000").await.is_ok()); + assert!(file.time_imported("000").await.is_ok()); } From 53f29a33d534942aae749234ded1a61bfe68eca9 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 15:40:45 +0100 Subject: [PATCH 08/11] Deprecate service_names_with_tags on file wrapper Signed-off-by: trivernis --- src/api_core/common.rs | 3 +++ src/lib.rs | 3 ++- src/wrapper/hydrus_file.rs | 25 +++++++++++++++++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/api_core/common.rs b/src/api_core/common.rs index 56668fa..3b6b3cb 100644 --- a/src/api_core/common.rs +++ b/src/api_core/common.rs @@ -54,7 +54,10 @@ pub struct FileMetadataInfo { pub is_local: bool, pub is_trashed: bool, pub known_urls: Vec, + #[deprecated] pub service_names_to_statuses_to_tags: HashMap>>, + pub service_keys_to_statuses_to_tags: HashMap>>, + #[deprecated] pub service_names_to_statuses_to_display_tags: HashMap>>, pub service_keys_to_statuses_to_display_tags: HashMap>>, } diff --git a/src/lib.rs b/src/lib.rs index 31fd877..3732cf0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,8 @@ //! let files = hydrus.search() //! .add_tag(Tag::from("character:megumin")) //! .add_tag(SystemTagBuilder::new().archive().build()) -//! .add_tag(SystemTagBuilder::new().tag_namespace_as_number("page", Comparator::Equal, 5).negate().build()) +//! .add_tag(SystemTagBuilder::new() +//! .tag_namespace_as_number("page", Comparator::Equal, 5).negate().build()) //! .add_or_chain( //! OrChainBuilder::new() //! .add_tag("summer".into()) diff --git a/src/wrapper/hydrus_file.rs b/src/wrapper/hydrus_file.rs index 1e612db..1021ab6 100644 --- a/src/wrapper/hydrus_file.rs +++ b/src/wrapper/hydrus_file.rs @@ -239,11 +239,15 @@ impl HydrusFile { self.client.disassociate_urls(urls, vec![hash]).await } - /// Returns map mapping lists of tags to services - pub async fn services_with_tags(&mut self) -> Result>> { + /// Returns map mapping lists of tags to services. + /// + /// Deprecation: Use [HydrusFile::services_with_tags] instead. + #[deprecated(note = "Deprecated in the official API. Use services_with_tags instead.")] + pub async fn service_names_with_tags(&mut self) -> Result>> { let metadata = self.metadata().await?; let mut tag_mappings = HashMap::new(); + #[allow(deprecated)] for (service, status_tags) in &metadata.service_names_to_statuses_to_tags { let mut tag_list = Vec::new(); @@ -256,6 +260,23 @@ impl HydrusFile { Ok(tag_mappings) } + /// Returns a mapping with service ids mapped to tags + pub async fn services_with_tags(&mut self) -> Result>> { + let metadata = self.metadata().await?; + let mut tag_mappings = HashMap::new(); + + for (service, status_tags) in &metadata.service_keys_to_statuses_to_tags { + let mut tag_list = Vec::new(); + + for (_, tags) in status_tags { + tag_list.append(&mut tags.into_iter().map(|t| t.into()).collect()) + } + tag_mappings.insert(ServiceIdentifier::Key(service.clone()), tag_list); + } + + Ok(tag_mappings) + } + /// Returns a list of all tags assigned to the file pub async fn tags(&mut self) -> Result> { let mut tag_list = Vec::new(); From 903acc884594c15cc54e005c84cf266c28c71317 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 15:48:40 +0100 Subject: [PATCH 09/11] Change tag import wrapper to use service identifiers instead of names Signed-off-by: trivernis --- README.md | 9 +++++---- src/lib.rs | 11 ++++++----- src/wrapper/builders/import_builder.rs | 16 ++++++---------- src/wrapper/hydrus_file.rs | 1 + tests/wrapper/test_import.rs | 7 +++++-- tests/wrapper/test_url.rs | 5 ++++- 6 files changed, 27 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9c91fe6..f6a3c02 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,13 @@ async fn main() { .run().await.unwrap(); for mut file in files { - file.add_tags(ServiceName::my_tags(), vec![Tag::from("ark mage")]).await.unwrap(); + file.add_tags(ServiceName::my_tags().into(), vec![Tag::from("ark mage")]).await.unwrap(); } let url = hydrus.import() .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") .page(PageIdentifier::name("My Import Page")) - .add_additional_tag(ServiceName::my_tags(), Tag::from("character:megumin")) + .add_additional_tag(ServiceName::my_tags().into(), Tag::from("character:megumin")) .show_page(true) .run().await.unwrap(); } @@ -67,6 +67,7 @@ async fn main() { use hydrus_api::Client; use hydrus_api::paths::adding_tags::{AddTagsRequestBuilder, TagAction}; use std::env; +use hydrus_api::api_core::common::ServiceIdentifier; #[tokio::main] async fn main() { @@ -81,9 +82,9 @@ async fn main() { let request = AddTagsRequestBuilder::default() .add_hash(hash) // for each tag the service has to be specified - .add_tags("my tags", vec!["beach".into(), "summer".into()]) + .add_tags(ServiceIdentifier::name("my tags"), vec!["beach".into(), "summer".into()]) // with tag actions tags can also be removed. It's especially useful for the PTR - .add_tag_with_action("my tags", "rain", TagAction::DeleteFromLocalService) + .add_tag_with_action(ServiceIdentifier::name("my tags"), "rain", TagAction::DeleteFromLocalService) .build(); client.add_tags(request).await.unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 3732cf0..4abc643 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! ## Hydrus Usage Example //! //! ``` -//! # use hydrus_api::{Hydrus, Client}; +//! use hydrus_api::{Hydrus, Client}; //! use std::env; //! use hydrus_api::wrapper::tag::Tag; //! use hydrus_api::wrapper::service::ServiceName; @@ -37,13 +37,13 @@ //! .run().await.unwrap(); //! //! for mut file in files { -//! file.add_tags(ServiceName::my_tags(), vec![Tag::from("ark mage")]).await.unwrap(); +//! file.add_tags(ServiceName::my_tags().into(), vec![Tag::from("ark mage")]).await.unwrap(); //! } //! //! let url = hydrus.import() //! .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") //! .page(PageIdentifier::name("My Import Page")) -//! .add_additional_tag(ServiceName::my_tags(), Tag::from("character:megumin")) +//! .add_additional_tag(ServiceName::my_tags().into(), Tag::from("character:megumin")) //! .show_page(true) //! .run().await.unwrap(); //! # } @@ -54,6 +54,7 @@ //! use hydrus_api::Client; //! use hydrus_api::api_core::adding_tags::{AddTagsRequestBuilder, TagAction}; //! use std::env; +//! use hydrus_api::api_core::common::ServiceIdentifier; //! # #[tokio::test] //! # async fn doctest() { //! @@ -68,9 +69,9 @@ //! let request = AddTagsRequestBuilder::default() //! .add_hash(hash) //! // for each tag the service has to be specified -//! .add_tags("my tags", vec!["beach".into(), "summer".into()]) +//! .add_tags(ServiceIdentifier::name("my tags"), vec!["beach".into(), "summer".into()]) //! // with tag actions tags can also be removed. It's especially useful for the PTR -//! .add_tag_with_action("my tags", "rain", TagAction::DeleteFromLocalService) +//! .add_tag_with_action(ServiceIdentifier::name("my tags"), "rain", TagAction::DeleteFromLocalService) //! .build(); //! //! client.add_tags(request).await.unwrap(); diff --git a/src/wrapper/builders/import_builder.rs b/src/wrapper/builders/import_builder.rs index a99b2aa..c8969e8 100644 --- a/src/wrapper/builders/import_builder.rs +++ b/src/wrapper/builders/import_builder.rs @@ -5,7 +5,6 @@ use crate::error::{Error, Result}; use crate::utils::tag_list_to_string_list; use crate::wrapper::hydrus_file::HydrusFile; use crate::wrapper::page::PageIdentifier; -use crate::wrapper::service::ServiceName; use crate::wrapper::tag::Tag; use crate::wrapper::url::Url; use crate::Client; @@ -78,7 +77,7 @@ pub struct UrlImportBuilder { page: Option, show_page: bool, filter_tags: Vec, - service_tag_mappings: HashMap>, + service_tag_mappings: HashMap>, } impl UrlImportBuilder { @@ -122,16 +121,16 @@ impl UrlImportBuilder { } /// Adds an additional tag for the imported file - pub fn add_additional_tag(self, service: ServiceName, tag: Tag) -> Self { + pub fn add_additional_tag(self, service: ServiceIdentifier, tag: Tag) -> Self { self.add_additional_tags(service, vec![tag]) } /// Adds multiple additional tags for the import - pub fn add_additional_tags(mut self, service: ServiceName, mut tags: Vec) -> Self { - if let Some(service_tags) = self.service_tag_mappings.get_mut(&service.0) { + pub fn add_additional_tags(mut self, service: ServiceIdentifier, mut tags: Vec) -> Self { + if let Some(service_tags) = self.service_tag_mappings.get_mut(&service) { service_tags.append(&mut tags); } else { - self.service_tag_mappings.insert(service.0, tags); + self.service_tag_mappings.insert(service, tags); } self @@ -142,10 +141,7 @@ impl UrlImportBuilder { let mut request = AddUrlRequestBuilder::default().url(&self.url); for (service, tags) in self.service_tag_mappings { - request = request.add_tags( - ServiceIdentifier::name(service), - tag_list_to_string_list(tags), - ); + request = request.add_tags(service, tag_list_to_string_list(tags)); } request = request.add_filter_tags(tag_list_to_string_list(self.filter_tags)); if let Some(page) = self.page { diff --git a/src/wrapper/hydrus_file.rs b/src/wrapper/hydrus_file.rs index 1021ab6..60c5cb8 100644 --- a/src/wrapper/hydrus_file.rs +++ b/src/wrapper/hydrus_file.rs @@ -212,6 +212,7 @@ impl HydrusFile { Ok(naive_time_imported) } + /// Returns the time the file was deleted for a specified file service pub async fn time_deleted>( &mut self, service_key: S, diff --git a/tests/wrapper/test_import.rs b/tests/wrapper/test_import.rs index 21a988e..2091783 100644 --- a/tests/wrapper/test_import.rs +++ b/tests/wrapper/test_import.rs @@ -39,8 +39,11 @@ async fn it_imports_urls() { .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") .page(PageIdentifier::name("Rusty Import")) .show_page(true) - .add_additional_tag(ServiceName::my_tags(), Tag::from("ark mage")) - .add_additional_tag(ServiceName::my_tags(), Tag::from("character:megumin")) + .add_additional_tag(ServiceName::my_tags().into(), Tag::from("ark mage")) + .add_additional_tag( + ServiceName::my_tags().into(), + Tag::from("character:megumin"), + ) .run() .await .unwrap(); diff --git a/tests/wrapper/test_url.rs b/tests/wrapper/test_url.rs index 700bad5..87ecb23 100644 --- a/tests/wrapper/test_url.rs +++ b/tests/wrapper/test_url.rs @@ -18,7 +18,10 @@ async fn it_imports() { url.import() .page(PageIdentifier::name("Rusty Import")) - .add_additional_tag(ServiceName::my_tags(), Tag::from("character:megumin")) + .add_additional_tag( + ServiceName::my_tags().into(), + Tag::from("character:megumin"), + ) .run() .await .unwrap(); From c6aa3f97f42fdb5ccd165aac804554f3f9401ca3 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 15:55:17 +0100 Subject: [PATCH 10/11] Add deprecation hint to service name usages Signed-off-by: trivernis --- src/api_core/common.rs | 8 ++++++++ tests/client/test_adding_tags.rs | 1 + tests/client/test_adding_urls.rs | 1 + 3 files changed, 10 insertions(+) diff --git a/src/api_core/common.rs b/src/api_core/common.rs index 3b6b3cb..3af1702 100644 --- a/src/api_core/common.rs +++ b/src/api_core/common.rs @@ -16,15 +16,23 @@ impl BasicServiceInfo { #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialOrd, PartialEq, Ord, Eq)] pub enum ServiceIdentifier { + /// Try to avoid using this variant as it will be removed from the interface + /// in the future Name(String), + /// The key variant of a service which should be the preferred variant. Key(String), } impl ServiceIdentifier { + /// Deprecation: use [ServiceIdentifier::key] instead. + #[deprecated( + note = "Deprecation in the official interface was mentioned. Use the service keys instead." + )] pub fn name(name: S) -> Self { Self::Name(name.to_string()) } + /// Constructs a new type of the key variant. pub fn key(key: S) -> Self { Self::Key(key.to_string()) } diff --git a/tests/client/test_adding_tags.rs b/tests/client/test_adding_tags.rs index 467d08f..c2797f8 100644 --- a/tests/client/test_adding_tags.rs +++ b/tests/client/test_adding_tags.rs @@ -19,6 +19,7 @@ async fn it_cleans_tags() { #[tokio::test] async fn it_adds_tags() { + #![allow(deprecated)] let client = common::get_client(); let request = AddTagsRequestBuilder::default() .add_hash("0000000000000000000000000000000000000000000000000000000000000000") // valid hash, I hope no files are affected diff --git a/tests/client/test_adding_urls.rs b/tests/client/test_adding_urls.rs index b262f9d..ef0939a 100644 --- a/tests/client/test_adding_urls.rs +++ b/tests/client/test_adding_urls.rs @@ -26,6 +26,7 @@ async fn it_returns_url_information() { #[tokio::test] async fn it_adds_urls() { + #![allow(deprecated)] let client = common::get_client(); let request = AddUrlRequestBuilder::default() .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") From ed2c8f9e0a3b4a3f851368745b00e5a2edaeecc7 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 27 Feb 2022 15:57:33 +0100 Subject: [PATCH 11/11] Increment version Signed-off-by: trivernis --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b0a2ddc..20c4412 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydrus-api" -version = "0.6.0" +version = "0.7.0" authors = ["trivernis "] edition = "2018" license = "Apache-2.0"