From f9e06a9af2c6a85e5babced68e202d3526e287ea Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 5 Sep 2021 11:04:23 +0200 Subject: [PATCH 1/4] Add support for or-chains Signed-off-by: trivernis --- README.md | 7 +++ src/api_core/client.rs | 12 +++-- src/api_core/searching_and_fetching_files.rs | 15 ++++++ src/lib.rs | 7 +++ src/utils.rs | 14 ++++++ src/wrapper/builders/mod.rs | 5 +- src/wrapper/builders/or_chain_builder.rs | 30 ++++++++++++ src/wrapper/builders/search_builder.rs | 28 +++++++++-- src/wrapper/mod.rs | 1 + src/wrapper/or_chain.rs | 46 +++++++++++++++++++ .../test_searching_and_fetching_files.rs | 10 +++- tests/wrapper/test_hydrus.rs | 7 +++ 12 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 src/wrapper/builders/or_chain_builder.rs create mode 100644 src/wrapper/or_chain.rs 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 From 3773ffdf8e74bc50dac7bd545d8d2e5dd55fadf4 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 5 Sep 2021 11:08:28 +0200 Subject: [PATCH 2/4] Add additional test for negated tags in or-chains Signed-off-by: trivernis --- tests/wrapper/test_hydrus.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/wrapper/test_hydrus.rs b/tests/wrapper/test_hydrus.rs index 8c2a84f..7f10325 100644 --- a/tests/wrapper/test_hydrus.rs +++ b/tests/wrapper/test_hydrus.rs @@ -2,6 +2,7 @@ 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::builders::tag_builder::TagBuilder; use hydrus_api::wrapper::service::{ServiceName, ServiceType}; use hydrus_api::wrapper::url::UrlType; @@ -44,6 +45,7 @@ async fn it_searches() { OrChainBuilder::new() .add_tag("summer".into()) .add_tag("winter".into()) + .add_tag(TagBuilder::new("inside").negate().build()) .build(), ) .sort_by(SortType::ModifiedTime) From 36d0853a8bfe82155da0d49870974e22649a3e29 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 5 Sep 2021 11:08:52 +0200 Subject: [PATCH 3/4] 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 6f3c35f..e96f2b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydrus-api" -version = "0.5.0" +version = "0.6.0" authors = ["trivernis "] edition = "2018" license = "Apache-2.0" From fa904761f58f5b31678dbdb16fc9c0744e7377ca Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 5 Sep 2021 11:26:17 +0200 Subject: [PATCH 4/4] Fix parsing of or chains from string Signed-off-by: trivernis --- Cargo.toml | 3 ++- src/wrapper/or_chain.rs | 14 +++++++++++--- src/wrapper/tag.rs | 9 ++++++--- tests/wrapper/mod.rs | 1 + tests/wrapper/test_or_chain.rs | 26 ++++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 tests/wrapper/test_or_chain.rs diff --git a/Cargo.toml b/Cargo.toml index e96f2b7..4d51952 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,12 @@ reqwest = {version = "0.11.4", features = ["json"]} log = "0.4.14" 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" -lazy_static = "1.4.0" [dev-dependencies.tokio] version = "1.8.0" diff --git a/src/wrapper/or_chain.rs b/src/wrapper/or_chain.rs index b9046c3..7afacf4 100644 --- a/src/wrapper/or_chain.rs +++ b/src/wrapper/or_chain.rs @@ -1,11 +1,15 @@ use crate::utils::tag_list_to_string_list; use crate::wrapper::tag::Tag; +use lazy_static::lazy_static; +use regex::Regex; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialOrd, PartialEq)] pub struct OrChain { tags: Vec, } +impl Eq for OrChain {} + impl OrChain { /// Creates a new or chain directly from a list of tags pub fn new(tags: Vec) -> Self { @@ -27,9 +31,12 @@ where S: AsRef, { fn from(s: S) -> Self { + lazy_static! { + static ref CHAIN_REGEX: Regex = Regex::new(r#"(\s|'|")or(\s|'|")"#).unwrap(); + } let s = s.as_ref().to_ascii_lowercase(); - let tags = s - .split("or") + let tags = CHAIN_REGEX + .split(&s) .map(|mut t| { t = t .trim_start() @@ -40,6 +47,7 @@ where }) .map(Tag::from) .collect(); + log::debug!("String parsed to or-chain {:?}", tags); Self { tags } } diff --git a/src/wrapper/tag.rs b/src/wrapper/tag.rs index efd48dc..c96193b 100644 --- a/src/wrapper/tag.rs +++ b/src/wrapper/tag.rs @@ -1,17 +1,20 @@ -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialOrd, PartialEq)] pub struct Tag { pub negated: bool, pub name: String, pub namespace: Option, } +impl Eq for Tag {} + impl From for Tag where S: AsRef, { fn from(value: S) -> Self { - let value = value.as_ref().trim(); - let negated = value.strip_prefix("-").is_some(); + let mut value = value.as_ref().trim(); + let negated = value.starts_with("-"); + value = value.trim_start_matches("-"); if let Some((namespace, tag)) = value.split_once(":") { Self { negated, diff --git a/tests/wrapper/mod.rs b/tests/wrapper/mod.rs index dc843f3..ba14b87 100644 --- a/tests/wrapper/mod.rs +++ b/tests/wrapper/mod.rs @@ -2,6 +2,7 @@ mod test_address; mod test_files; mod test_hydrus; mod test_import; +mod test_or_chain; mod test_page; mod test_service; mod test_tags; diff --git a/tests/wrapper/test_or_chain.rs b/tests/wrapper/test_or_chain.rs new file mode 100644 index 0000000..ee4feee --- /dev/null +++ b/tests/wrapper/test_or_chain.rs @@ -0,0 +1,26 @@ +use super::super::common; +use hydrus_api::wrapper::builders::or_chain_builder::OrChainBuilder; +use hydrus_api::wrapper::builders::tag_builder::TagBuilder; +use hydrus_api::wrapper::or_chain::OrChain; + +#[test] +fn it_parses_from_string() { + common::setup(); + let chain_string = + "'character:megumin' or 'character:aqua' OR '-character:hatsune miku'or 'terminator'"; + let chain = OrChain::from(chain_string); + assert_eq!( + chain, + OrChainBuilder::new() + .add_tag("character:megumin".into()) + .add_tag("character:aqua".into()) + .add_tag( + TagBuilder::new("hatsune miku") + .namespace("character") + .negate() + .build() + ) + .add_tag("terminator".into()) + .build() + ); +}