diff --git a/README.md b/README.md index 04d2258..9c91fe6 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ use hydrus_api::wrapper::service::ServiceName; use hydrus_api::wrapper::hydrus_file::FileStatus; use hydrus_api::wrapper::page::PageIdentifier; use hydrus_api::wrapper::builders::search_builder::SortType; +use hydrus_api::wrapper::builders::or_chain_builder::OrChainBuilder; use hydrus_api::wrapper::builders::tag_builder::{ SystemTagBuilder, Comparator }; @@ -38,6 +39,12 @@ async fn main() { .add_tag(Tag::from("character:megumin")) .add_tag(SystemTagBuilder::new().archive().build()) .add_tag(SystemTagBuilder::new().number_of_tags(Comparator::Greater, 12).build()) + .add_or_chain( + OrChainBuilder::new() + .add_tag("summer".into()) + .add_tag("winter".into()) + .build(), + ) .sort(SortType::ModifiedTime) .run().await.unwrap(); diff --git a/src/api_core/client.rs b/src/api_core/client.rs index f3b288f..1315ba4 100644 --- a/src/api_core/client.rs +++ b/src/api_core/client.rs @@ -22,11 +22,13 @@ use crate::api_core::managing_pages::{ }; use crate::api_core::searching_and_fetching_files::{ FileMetadata, FileMetadataResponse, FileSearchOptions, GetFile, SearchFiles, - SearchFilesResponse, + SearchFilesResponse, SearchQueryEntry, }; use crate::api_core::Endpoint; use crate::error::{Error, Result}; -use crate::utils::{number_list_to_json_array, string_list_to_json_array}; +use crate::utils::{ + number_list_to_json_array, search_query_list_to_json_array, string_list_to_json_array, +}; use reqwest::Response; use serde::de::DeserializeOwned; use serde::Serialize; @@ -225,12 +227,12 @@ impl Client { /// Searches for files in the inbox, the archive or both pub async fn search_files( &self, - tags: Vec, + query: Vec, options: FileSearchOptions, ) -> Result { - log::trace!("Searching for files with tags {:?}", tags); + log::trace!("Searching for files with tags {:?}", query); let mut args = options.into_query_args(); - args.push(("tags", string_list_to_json_array(tags))); + args.push(("tags", search_query_list_to_json_array(query))); self.get_and_parse::(&args) .await } diff --git a/src/api_core/searching_and_fetching_files.rs b/src/api_core/searching_and_fetching_files.rs index 677858c..a7a56e6 100644 --- a/src/api_core/searching_and_fetching_files.rs +++ b/src/api_core/searching_and_fetching_files.rs @@ -138,3 +138,18 @@ impl Endpoint for GetFile { String::from("get_files/file") } } + +#[derive(Clone, Debug)] +pub enum SearchQueryEntry { + Tag(String), + OrChain(Vec), +} + +impl From for SearchQueryEntry +where + S: ToString, +{ + fn from(s: S) -> Self { + Self::Tag(s.to_string()) + } +} diff --git a/src/lib.rs b/src/lib.rs index c9f8f5f..31fd877 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ //! use hydrus_api::wrapper::page::PageIdentifier; //! use hydrus_api::wrapper::builders::tag_builder::{SystemTagBuilder, Comparator}; //! use hydrus_api::wrapper::builders::search_builder::SortType; +//! use hydrus_api::wrapper::builders::or_chain_builder::OrChainBuilder; //! //! # #[tokio::test] //! # async fn doctest() { @@ -24,6 +25,12 @@ //! .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_or_chain( +//! OrChainBuilder::new() +//! .add_tag("summer".into()) +//! .add_tag("winter".into()) +//! .build(), +//! ) //! .sort_by(SortType::NumberOfPixels) //! .sort_descending() //! .run().await.unwrap(); diff --git a/src/utils.rs b/src/utils.rs index 2fa9107..9b176fd 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,21 @@ use crate::api_core::common::FileIdentifier; +use crate::api_core::searching_and_fetching_files::SearchQueryEntry; use crate::wrapper::tag::Tag; use chrono::{Datelike, Duration}; +/// Converts a list of Search Query entries into a json array +pub(crate) fn search_query_list_to_json_array(l: Vec) -> String { + let entry_list: Vec = l + .into_iter() + .map(|e| match e { + SearchQueryEntry::Tag(t) => format!("\"{}\"", t), + SearchQueryEntry::OrChain(c) => string_list_to_json_array(c), + }) + .collect(); + + format!("[{}]", entry_list.join(",")) +} + pub(crate) fn string_list_to_json_array(l: Vec) -> String { format!("[\"{}\"]", l.join("\",\"")) } diff --git a/src/wrapper/builders/mod.rs b/src/wrapper/builders/mod.rs index 0584473..5e65902 100644 --- a/src/wrapper/builders/mod.rs +++ b/src/wrapper/builders/mod.rs @@ -1,4 +1,5 @@ pub mod import_builder; -pub mod tagging_builder; -pub mod tag_builder; +pub mod or_chain_builder; pub mod search_builder; +pub mod tag_builder; +pub mod tagging_builder; diff --git a/src/wrapper/builders/or_chain_builder.rs b/src/wrapper/builders/or_chain_builder.rs new file mode 100644 index 0000000..0850a86 --- /dev/null +++ b/src/wrapper/builders/or_chain_builder.rs @@ -0,0 +1,30 @@ +use crate::wrapper::or_chain::OrChain; +use crate::wrapper::tag::Tag; + +#[derive(Debug)] +pub struct OrChainBuilder { + tags: Vec, +} + +impl OrChainBuilder { + pub fn new() -> Self { + Self { tags: Vec::new() } + } + + /// Adds a tag to the or expression + pub fn add_tag(mut self, tag: Tag) -> Self { + self.tags.push(tag); + self + } + + /// Adds multiple tags to the or expression + pub fn add_tags(mut self, mut tags: Vec) -> Self { + self.tags.append(&mut tags); + self + } + + /// Builds the or chain + pub fn build(self) -> OrChain { + OrChain::new(self.tags) + } +} diff --git a/src/wrapper/builders/search_builder.rs b/src/wrapper/builders/search_builder.rs index 7c6c35f..86a5d06 100644 --- a/src/wrapper/builders/search_builder.rs +++ b/src/wrapper/builders/search_builder.rs @@ -1,7 +1,7 @@ -use crate::api_core::searching_and_fetching_files::FileSearchOptions; +use crate::api_core::searching_and_fetching_files::{FileSearchOptions, SearchQueryEntry}; use crate::error::Result; -use crate::utils::tag_list_to_string_list; use crate::wrapper::hydrus_file::HydrusFile; +use crate::wrapper::or_chain::OrChain; use crate::wrapper::service::ServiceName; use crate::wrapper::tag::Tag; use crate::Client; @@ -30,6 +30,7 @@ pub enum SortType { pub struct SearchBuilder { client: Client, tags: Vec, + or_chains: Vec, options: FileSearchOptions, } @@ -38,6 +39,7 @@ impl SearchBuilder { Self { client, tags: Vec::new(), + or_chains: Vec::new(), options: FileSearchOptions::new(), } } @@ -54,6 +56,12 @@ impl SearchBuilder { self } + /// Adds a new or chain + pub fn add_or_chain(mut self, chain: OrChain) -> Self { + self.or_chains.push(chain); + self + } + /// Sets the sort type pub fn sort_by(mut self, sort_type: SortType) -> Self { self.options = self.options.sort_type(sort_type as u8); @@ -101,9 +109,19 @@ impl SearchBuilder { /// Runs the search pub async fn run(self) -> Result> { let client = self.client.clone(); - let response = client - .search_files(tag_list_to_string_list(self.tags), self.options) - .await?; + let mut entries: Vec = self + .tags + .into_iter() + .map(|t| SearchQueryEntry::Tag(t.to_string())) + .collect(); + entries.append( + &mut self + .or_chains + .into_iter() + .map(|c| SearchQueryEntry::OrChain(c.into_string_list())) + .collect(), + ); + let response = client.search_files(entries, self.options).await?; let files = response .file_ids .into_iter() diff --git a/src/wrapper/mod.rs b/src/wrapper/mod.rs index 32d5e62..55ac67b 100644 --- a/src/wrapper/mod.rs +++ b/src/wrapper/mod.rs @@ -2,6 +2,7 @@ pub mod address; pub mod builders; pub mod hydrus; pub mod hydrus_file; +pub mod or_chain; pub mod page; pub mod service; pub mod tag; diff --git a/src/wrapper/or_chain.rs b/src/wrapper/or_chain.rs new file mode 100644 index 0000000..b9046c3 --- /dev/null +++ b/src/wrapper/or_chain.rs @@ -0,0 +1,46 @@ +use crate::utils::tag_list_to_string_list; +use crate::wrapper::tag::Tag; + +#[derive(Clone, Debug)] +pub struct OrChain { + tags: Vec, +} + +impl OrChain { + /// Creates a new or chain directly from a list of tags + pub fn new(tags: Vec) -> Self { + Self { tags } + } + + /// Returns the tags of this or chain + pub fn tags(&self) -> &Vec { + &self.tags + } + + pub(crate) fn into_string_list(self) -> Vec { + tag_list_to_string_list(self.tags) + } +} + +impl From for OrChain +where + S: AsRef, +{ + fn from(s: S) -> Self { + let s = s.as_ref().to_ascii_lowercase(); + let tags = s + .split("or") + .map(|mut t| { + t = t + .trim_start() + .trim_start_matches("'") + .trim_start_matches("\""); + t = t.trim_end().trim_end_matches("'").trim_end_matches("\""); + t + }) + .map(Tag::from) + .collect(); + + Self { tags } + } +} diff --git a/tests/client/test_searching_and_fetching_files.rs b/tests/client/test_searching_and_fetching_files.rs index 9859b6b..89dcd14 100644 --- a/tests/client/test_searching_and_fetching_files.rs +++ b/tests/client/test_searching_and_fetching_files.rs @@ -1,7 +1,7 @@ use super::super::common; use hydrus_api::api_core::common::FileIdentifier; use hydrus_api::api_core::file_sort_type::SORT_FILE_PIXEL_COUNT; -use hydrus_api::api_core::searching_and_fetching_files::FileSearchOptions; +use hydrus_api::api_core::searching_and_fetching_files::{FileSearchOptions, SearchQueryEntry}; #[tokio::test] async fn is_searches_files() { @@ -11,7 +11,13 @@ async fn is_searches_files() { .tag_service_name("public tag repository") .file_service_name("all known files"); client - .search_files(vec!["beach".to_string()], options) + .search_files( + vec![ + "beach".into(), + SearchQueryEntry::OrChain(vec!["summer".to_string(), "winter".to_string()]), + ], + options, + ) .await .unwrap(); } diff --git a/tests/wrapper/test_hydrus.rs b/tests/wrapper/test_hydrus.rs index e4866b8..8c2a84f 100644 --- a/tests/wrapper/test_hydrus.rs +++ b/tests/wrapper/test_hydrus.rs @@ -1,5 +1,6 @@ use super::super::common; use hydrus_api::api_core::adding_tags::TagAction; +use hydrus_api::wrapper::builders::or_chain_builder::OrChainBuilder; use hydrus_api::wrapper::builders::search_builder::SortType; use hydrus_api::wrapper::service::{ServiceName, ServiceType}; use hydrus_api::wrapper::url::UrlType; @@ -39,6 +40,12 @@ async fn it_searches() { hydrus .search() .add_tag("character:megumin".into()) + .add_or_chain( + OrChainBuilder::new() + .add_tag("summer".into()) + .add_tag("winter".into()) + .build(), + ) .sort_by(SortType::ModifiedTime) .run() .await