treewide: fix various UI bugs; optimize crypto dependencies & format
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
This commit is contained in:
parent
764aafa88d
commit
3ccddce7fd
178 changed files with 58342 additions and 54241 deletions
|
|
@ -1,553 +1,524 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use winnow::combinator::{alt, delimited, preceded, repeat};
|
||||
use winnow::token::{take_till, take_while};
|
||||
use winnow::{ModalResult, Parser};
|
||||
use winnow::{
|
||||
ModalResult,
|
||||
Parser,
|
||||
combinator::{alt, delimited, preceded, repeat},
|
||||
token::{take_till, take_while},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SearchQuery {
|
||||
FullText(String),
|
||||
FieldMatch {
|
||||
field: String,
|
||||
value: String,
|
||||
},
|
||||
And(Vec<SearchQuery>),
|
||||
Or(Vec<SearchQuery>),
|
||||
Not(Box<SearchQuery>),
|
||||
Prefix(String),
|
||||
Fuzzy(String),
|
||||
TypeFilter(String),
|
||||
TagFilter(String),
|
||||
/// Range query: field:start..end (inclusive)
|
||||
RangeQuery {
|
||||
field: String,
|
||||
start: Option<i64>,
|
||||
end: Option<i64>,
|
||||
},
|
||||
/// Comparison query: field:>value, field:<value, field:>=value, field:<=value
|
||||
CompareQuery {
|
||||
field: String,
|
||||
op: CompareOp,
|
||||
value: i64,
|
||||
},
|
||||
/// Date query: created:today, modified:last-week, etc.
|
||||
DateQuery {
|
||||
field: String,
|
||||
value: DateValue,
|
||||
},
|
||||
FullText(String),
|
||||
FieldMatch {
|
||||
field: String,
|
||||
value: String,
|
||||
},
|
||||
And(Vec<SearchQuery>),
|
||||
Or(Vec<SearchQuery>),
|
||||
Not(Box<SearchQuery>),
|
||||
Prefix(String),
|
||||
Fuzzy(String),
|
||||
TypeFilter(String),
|
||||
TagFilter(String),
|
||||
/// Range query: field:start..end (inclusive)
|
||||
RangeQuery {
|
||||
field: String,
|
||||
start: Option<i64>,
|
||||
end: Option<i64>,
|
||||
},
|
||||
/// Comparison query: field:>value, field:<value, field:>=value, field:<=value
|
||||
CompareQuery {
|
||||
field: String,
|
||||
op: CompareOp,
|
||||
value: i64,
|
||||
},
|
||||
/// Date query: created:today, modified:last-week, etc.
|
||||
DateQuery {
|
||||
field: String,
|
||||
value: DateValue,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CompareOp {
|
||||
GreaterThan,
|
||||
GreaterOrEqual,
|
||||
LessThan,
|
||||
LessOrEqual,
|
||||
GreaterThan,
|
||||
GreaterOrEqual,
|
||||
LessThan,
|
||||
LessOrEqual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DateValue {
|
||||
Today,
|
||||
Yesterday,
|
||||
ThisWeek,
|
||||
LastWeek,
|
||||
ThisMonth,
|
||||
LastMonth,
|
||||
ThisYear,
|
||||
LastYear,
|
||||
/// Days ago: last-7d, last-30d
|
||||
DaysAgo(u32),
|
||||
Today,
|
||||
Yesterday,
|
||||
ThisWeek,
|
||||
LastWeek,
|
||||
ThisMonth,
|
||||
LastMonth,
|
||||
ThisYear,
|
||||
LastYear,
|
||||
/// Days ago: last-7d, last-30d
|
||||
DaysAgo(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SearchRequest {
|
||||
pub query: SearchQuery,
|
||||
pub sort: SortOrder,
|
||||
pub pagination: crate::model::Pagination,
|
||||
pub query: SearchQuery,
|
||||
pub sort: SortOrder,
|
||||
pub pagination: crate::model::Pagination,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SearchResults {
|
||||
pub items: Vec<crate::model::MediaItem>,
|
||||
pub total_count: u64,
|
||||
pub items: Vec<crate::model::MediaItem>,
|
||||
pub total_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[derive(Default)]
|
||||
pub enum SortOrder {
|
||||
#[default]
|
||||
Relevance,
|
||||
DateAsc,
|
||||
DateDesc,
|
||||
NameAsc,
|
||||
NameDesc,
|
||||
SizeAsc,
|
||||
SizeDesc,
|
||||
#[default]
|
||||
Relevance,
|
||||
DateAsc,
|
||||
DateDesc,
|
||||
NameAsc,
|
||||
NameDesc,
|
||||
SizeAsc,
|
||||
SizeDesc,
|
||||
}
|
||||
|
||||
fn ws<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
|
||||
take_while(0.., ' ').parse_next(input)
|
||||
take_while(0.., ' ').parse_next(input)
|
||||
}
|
||||
|
||||
fn quoted_string(input: &mut &str) -> ModalResult<String> {
|
||||
delimited('"', take_till(0.., '"'), '"')
|
||||
.map(|s: &str| s.to_string())
|
||||
.parse_next(input)
|
||||
delimited('"', take_till(0.., '"'), '"')
|
||||
.map(|s: &str| s.to_string())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn bare_word(input: &mut &str) -> ModalResult<String> {
|
||||
take_while(1.., |c: char| !c.is_whitespace() && c != ')' && c != '(')
|
||||
.map(|s: &str| s.to_string())
|
||||
.parse_next(input)
|
||||
take_while(1.., |c: char| !c.is_whitespace() && c != ')' && c != '(')
|
||||
.map(|s: &str| s.to_string())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn word_or_quoted(input: &mut &str) -> ModalResult<String> {
|
||||
alt((quoted_string, bare_word)).parse_next(input)
|
||||
alt((quoted_string, bare_word)).parse_next(input)
|
||||
}
|
||||
|
||||
fn not_expr(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
preceded(('-', ws), atom)
|
||||
.map(|q| SearchQuery::Not(Box::new(q)))
|
||||
.parse_next(input)
|
||||
preceded(('-', ws), atom)
|
||||
.map(|q| SearchQuery::Not(Box::new(q)))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a date value like "today", "yesterday", "last-week", "last-30d"
|
||||
fn parse_date_value(s: &str) -> Option<DateValue> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"today" => Some(DateValue::Today),
|
||||
"yesterday" => Some(DateValue::Yesterday),
|
||||
"this-week" | "thisweek" => Some(DateValue::ThisWeek),
|
||||
"last-week" | "lastweek" => Some(DateValue::LastWeek),
|
||||
"this-month" | "thismonth" => Some(DateValue::ThisMonth),
|
||||
"last-month" | "lastmonth" => Some(DateValue::LastMonth),
|
||||
"this-year" | "thisyear" => Some(DateValue::ThisYear),
|
||||
"last-year" | "lastyear" => Some(DateValue::LastYear),
|
||||
other => {
|
||||
// Try to parse "last-Nd" format (e.g., "last-7d", "last-30d")
|
||||
if let Some(rest) = other.strip_prefix("last-")
|
||||
&& let Some(days_str) = rest.strip_suffix('d')
|
||||
&& let Ok(days) = days_str.parse::<u32>()
|
||||
{
|
||||
return Some(DateValue::DaysAgo(days));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
match s.to_lowercase().as_str() {
|
||||
"today" => Some(DateValue::Today),
|
||||
"yesterday" => Some(DateValue::Yesterday),
|
||||
"this-week" | "thisweek" => Some(DateValue::ThisWeek),
|
||||
"last-week" | "lastweek" => Some(DateValue::LastWeek),
|
||||
"this-month" | "thismonth" => Some(DateValue::ThisMonth),
|
||||
"last-month" | "lastmonth" => Some(DateValue::LastMonth),
|
||||
"this-year" | "thisyear" => Some(DateValue::ThisYear),
|
||||
"last-year" | "lastyear" => Some(DateValue::LastYear),
|
||||
other => {
|
||||
// Try to parse "last-Nd" format (e.g., "last-7d", "last-30d")
|
||||
if let Some(rest) = other.strip_prefix("last-")
|
||||
&& let Some(days_str) = rest.strip_suffix('d')
|
||||
&& let Ok(days) = days_str.parse::<u32>()
|
||||
{
|
||||
return Some(DateValue::DaysAgo(days));
|
||||
}
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse size strings like "10MB", "1GB", "500KB" to bytes
|
||||
fn parse_size_value(s: &str) -> Option<i64> {
|
||||
let s = s.to_uppercase();
|
||||
if let Some(num) = s.strip_suffix("GB") {
|
||||
num.parse::<i64>().ok().map(|n| n * 1024 * 1024 * 1024)
|
||||
} else if let Some(num) = s.strip_suffix("MB") {
|
||||
num.parse::<i64>().ok().map(|n| n * 1024 * 1024)
|
||||
} else if let Some(num) = s.strip_suffix("KB") {
|
||||
num.parse::<i64>().ok().map(|n| n * 1024)
|
||||
} else if let Some(num) = s.strip_suffix('B') {
|
||||
num.parse::<i64>().ok()
|
||||
} else {
|
||||
s.parse::<i64>().ok()
|
||||
}
|
||||
let s = s.to_uppercase();
|
||||
if let Some(num) = s.strip_suffix("GB") {
|
||||
num.parse::<i64>().ok().map(|n| n * 1024 * 1024 * 1024)
|
||||
} else if let Some(num) = s.strip_suffix("MB") {
|
||||
num.parse::<i64>().ok().map(|n| n * 1024 * 1024)
|
||||
} else if let Some(num) = s.strip_suffix("KB") {
|
||||
num.parse::<i64>().ok().map(|n| n * 1024)
|
||||
} else if let Some(num) = s.strip_suffix('B') {
|
||||
num.parse::<i64>().ok()
|
||||
} else {
|
||||
s.parse::<i64>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn field_match(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
let field_name =
|
||||
take_while(1.., |c: char| c.is_alphanumeric() || c == '_').map(|s: &str| s.to_string());
|
||||
(field_name, ':', word_or_quoted)
|
||||
.map(|(field, _, value)| {
|
||||
// Handle special field types
|
||||
match field.as_str() {
|
||||
"type" => return SearchQuery::TypeFilter(value),
|
||||
"tag" => return SearchQuery::TagFilter(value),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Check for range queries: field:start..end
|
||||
if value.contains("..") {
|
||||
let parts: Vec<&str> = value.split("..").collect();
|
||||
if parts.len() == 2 {
|
||||
let start = if parts[0].is_empty() {
|
||||
None
|
||||
} else if field == "size" {
|
||||
parse_size_value(parts[0])
|
||||
} else {
|
||||
parts[0].parse().ok()
|
||||
};
|
||||
let end = if parts[1].is_empty() {
|
||||
None
|
||||
} else if field == "size" {
|
||||
parse_size_value(parts[1])
|
||||
} else {
|
||||
parts[1].parse().ok()
|
||||
};
|
||||
return SearchQuery::RangeQuery { field, start, end };
|
||||
}
|
||||
}
|
||||
|
||||
// Check for comparison queries: >=, <=, >, <
|
||||
if let Some(rest) = value.strip_prefix(">=") {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::GreaterOrEqual,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix("<=") {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::LessOrEqual,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix('>') {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::GreaterThan,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix('<') {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::LessThan,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for date queries on created/modified fields
|
||||
if (field == "created" || field == "modified")
|
||||
&& let Some(date_val) = parse_date_value(&value)
|
||||
{
|
||||
return SearchQuery::DateQuery {
|
||||
field,
|
||||
value: date_val,
|
||||
};
|
||||
}
|
||||
|
||||
// Default: simple field match
|
||||
SearchQuery::FieldMatch { field, value }
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn prefix_expr(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
let word = take_while(1.., |c: char| {
|
||||
!c.is_whitespace() && c != ')' && c != '(' && c != '*'
|
||||
})
|
||||
let field_name = take_while(1.., |c: char| c.is_alphanumeric() || c == '_')
|
||||
.map(|s: &str| s.to_string());
|
||||
(word, '*')
|
||||
.map(|(w, _)| SearchQuery::Prefix(w))
|
||||
.parse_next(input)
|
||||
}
|
||||
(field_name, ':', word_or_quoted)
|
||||
.map(|(field, _, value)| {
|
||||
// Handle special field types
|
||||
match field.as_str() {
|
||||
"type" => return SearchQuery::TypeFilter(value),
|
||||
"tag" => return SearchQuery::TagFilter(value),
|
||||
_ => {},
|
||||
}
|
||||
|
||||
fn fuzzy_expr(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
let word = take_while(1.., |c: char| {
|
||||
!c.is_whitespace() && c != ')' && c != '(' && c != '~'
|
||||
// Check for range queries: field:start..end
|
||||
if value.contains("..") {
|
||||
let parts: Vec<&str> = value.split("..").collect();
|
||||
if parts.len() == 2 {
|
||||
let start = if parts[0].is_empty() {
|
||||
None
|
||||
} else if field == "size" {
|
||||
parse_size_value(parts[0])
|
||||
} else {
|
||||
parts[0].parse().ok()
|
||||
};
|
||||
let end = if parts[1].is_empty() {
|
||||
None
|
||||
} else if field == "size" {
|
||||
parse_size_value(parts[1])
|
||||
} else {
|
||||
parts[1].parse().ok()
|
||||
};
|
||||
return SearchQuery::RangeQuery { field, start, end };
|
||||
}
|
||||
}
|
||||
|
||||
// Check for comparison queries: >=, <=, >, <
|
||||
if let Some(rest) = value.strip_prefix(">=") {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::GreaterOrEqual,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix("<=") {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::LessOrEqual,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix('>') {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::GreaterThan,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix('<') {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::LessThan,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for date queries on created/modified fields
|
||||
if (field == "created" || field == "modified")
|
||||
&& let Some(date_val) = parse_date_value(&value)
|
||||
{
|
||||
return SearchQuery::DateQuery {
|
||||
field,
|
||||
value: date_val,
|
||||
};
|
||||
}
|
||||
|
||||
// Default: simple field match
|
||||
SearchQuery::FieldMatch { field, value }
|
||||
})
|
||||
.map(|s: &str| s.to_string());
|
||||
(word, '~')
|
||||
.map(|(w, _)| SearchQuery::Fuzzy(w))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn paren_expr(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
delimited(('(', ws), or_expr, (ws, ')')).parse_next(input)
|
||||
}
|
||||
|
||||
fn not_or_keyword(input: &mut &str) -> ModalResult<()> {
|
||||
if let Some(rest) = input.strip_prefix("OR")
|
||||
&& (rest.is_empty() || rest.starts_with(' ') || rest.starts_with(')'))
|
||||
{
|
||||
return Err(winnow::error::ErrMode::Backtrack(
|
||||
winnow::error::ContextError::new(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn full_text(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
not_or_keyword.parse_next(input)?;
|
||||
word_or_quoted.map(SearchQuery::FullText).parse_next(input)
|
||||
}
|
||||
|
||||
fn atom(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
alt((
|
||||
paren_expr,
|
||||
not_expr,
|
||||
field_match,
|
||||
prefix_expr,
|
||||
fuzzy_expr,
|
||||
full_text,
|
||||
))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn prefix_expr(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
let word = take_while(1.., |c: char| {
|
||||
!c.is_whitespace() && c != ')' && c != '(' && c != '*'
|
||||
})
|
||||
.map(|s: &str| s.to_string());
|
||||
(word, '*')
|
||||
.map(|(w, _)| SearchQuery::Prefix(w))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn fuzzy_expr(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
let word = take_while(1.., |c: char| {
|
||||
!c.is_whitespace() && c != ')' && c != '(' && c != '~'
|
||||
})
|
||||
.map(|s: &str| s.to_string());
|
||||
(word, '~')
|
||||
.map(|(w, _)| SearchQuery::Fuzzy(w))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn paren_expr(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
delimited(('(', ws), or_expr, (ws, ')')).parse_next(input)
|
||||
}
|
||||
|
||||
fn not_or_keyword(input: &mut &str) -> ModalResult<()> {
|
||||
if let Some(rest) = input.strip_prefix("OR")
|
||||
&& (rest.is_empty() || rest.starts_with(' ') || rest.starts_with(')'))
|
||||
{
|
||||
return Err(winnow::error::ErrMode::Backtrack(
|
||||
winnow::error::ContextError::new(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn full_text(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
not_or_keyword.parse_next(input)?;
|
||||
word_or_quoted.map(SearchQuery::FullText).parse_next(input)
|
||||
}
|
||||
|
||||
fn atom(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
alt((
|
||||
paren_expr,
|
||||
not_expr,
|
||||
field_match,
|
||||
prefix_expr,
|
||||
fuzzy_expr,
|
||||
full_text,
|
||||
))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn and_expr(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
let first = atom.parse_next(input)?;
|
||||
let rest: Vec<SearchQuery> = repeat(0.., preceded(ws, atom)).parse_next(input)?;
|
||||
if rest.is_empty() {
|
||||
Ok(first)
|
||||
} else {
|
||||
let mut terms = vec![first];
|
||||
terms.extend(rest);
|
||||
Ok(SearchQuery::And(terms))
|
||||
}
|
||||
let first = atom.parse_next(input)?;
|
||||
let rest: Vec<SearchQuery> =
|
||||
repeat(0.., preceded(ws, atom)).parse_next(input)?;
|
||||
if rest.is_empty() {
|
||||
Ok(first)
|
||||
} else {
|
||||
let mut terms = vec![first];
|
||||
terms.extend(rest);
|
||||
Ok(SearchQuery::And(terms))
|
||||
}
|
||||
}
|
||||
|
||||
fn or_expr(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
let first = and_expr.parse_next(input)?;
|
||||
let rest: Vec<SearchQuery> =
|
||||
repeat(0.., preceded((ws, "OR", ws), and_expr)).parse_next(input)?;
|
||||
if rest.is_empty() {
|
||||
Ok(first)
|
||||
} else {
|
||||
let mut terms = vec![first];
|
||||
terms.extend(rest);
|
||||
Ok(SearchQuery::Or(terms))
|
||||
}
|
||||
let first = and_expr.parse_next(input)?;
|
||||
let rest: Vec<SearchQuery> =
|
||||
repeat(0.., preceded((ws, "OR", ws), and_expr)).parse_next(input)?;
|
||||
if rest.is_empty() {
|
||||
Ok(first)
|
||||
} else {
|
||||
let mut terms = vec![first];
|
||||
terms.extend(rest);
|
||||
Ok(SearchQuery::Or(terms))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_search_query(input: &str) -> crate::error::Result<SearchQuery> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(SearchQuery::FullText(String::new()));
|
||||
}
|
||||
let mut input = trimmed;
|
||||
or_expr
|
||||
.parse_next(&mut input)
|
||||
.map_err(|e| crate::error::PinakesError::SearchParse(format!("{e}")))
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(SearchQuery::FullText(String::new()));
|
||||
}
|
||||
let mut input = trimmed;
|
||||
or_expr
|
||||
.parse_next(&mut input)
|
||||
.map_err(|e| crate::error::PinakesError::SearchParse(format!("{e}")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simple_text() {
|
||||
let q = parse_search_query("hello").unwrap();
|
||||
assert_eq!(q, SearchQuery::FullText("hello".into()));
|
||||
}
|
||||
#[test]
|
||||
fn test_simple_text() {
|
||||
let q = parse_search_query("hello").unwrap();
|
||||
assert_eq!(q, SearchQuery::FullText("hello".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_field_match() {
|
||||
let q = parse_search_query("artist:Beatles").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::FieldMatch {
|
||||
field: "artist".into(),
|
||||
value: "Beatles".into()
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_field_match() {
|
||||
let q = parse_search_query("artist:Beatles").unwrap();
|
||||
assert_eq!(q, SearchQuery::FieldMatch {
|
||||
field: "artist".into(),
|
||||
value: "Beatles".into(),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_type_filter() {
|
||||
let q = parse_search_query("type:pdf").unwrap();
|
||||
assert_eq!(q, SearchQuery::TypeFilter("pdf".into()));
|
||||
}
|
||||
#[test]
|
||||
fn test_type_filter() {
|
||||
let q = parse_search_query("type:pdf").unwrap();
|
||||
assert_eq!(q, SearchQuery::TypeFilter("pdf".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag_filter() {
|
||||
let q = parse_search_query("tag:music").unwrap();
|
||||
assert_eq!(q, SearchQuery::TagFilter("music".into()));
|
||||
}
|
||||
#[test]
|
||||
fn test_tag_filter() {
|
||||
let q = parse_search_query("tag:music").unwrap();
|
||||
assert_eq!(q, SearchQuery::TagFilter("music".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_and_implicit() {
|
||||
let q = parse_search_query("hello world").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::And(vec![
|
||||
SearchQuery::FullText("hello".into()),
|
||||
SearchQuery::FullText("world".into()),
|
||||
])
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_and_implicit() {
|
||||
let q = parse_search_query("hello world").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::And(vec![
|
||||
SearchQuery::FullText("hello".into()),
|
||||
SearchQuery::FullText("world".into()),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_or() {
|
||||
let q = parse_search_query("hello OR world").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::Or(vec![
|
||||
SearchQuery::FullText("hello".into()),
|
||||
SearchQuery::FullText("world".into()),
|
||||
])
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_or() {
|
||||
let q = parse_search_query("hello OR world").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::Or(vec![
|
||||
SearchQuery::FullText("hello".into()),
|
||||
SearchQuery::FullText("world".into()),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not() {
|
||||
let q = parse_search_query("-excluded").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::Not(Box::new(SearchQuery::FullText("excluded".into())))
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_not() {
|
||||
let q = parse_search_query("-excluded").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::Not(Box::new(SearchQuery::FullText("excluded".into())))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefix() {
|
||||
let q = parse_search_query("hel*").unwrap();
|
||||
assert_eq!(q, SearchQuery::Prefix("hel".into()));
|
||||
}
|
||||
#[test]
|
||||
fn test_prefix() {
|
||||
let q = parse_search_query("hel*").unwrap();
|
||||
assert_eq!(q, SearchQuery::Prefix("hel".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzzy() {
|
||||
let q = parse_search_query("hello~").unwrap();
|
||||
assert_eq!(q, SearchQuery::Fuzzy("hello".into()));
|
||||
}
|
||||
#[test]
|
||||
fn test_fuzzy() {
|
||||
let q = parse_search_query("hello~").unwrap();
|
||||
assert_eq!(q, SearchQuery::Fuzzy("hello".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quoted() {
|
||||
let q = parse_search_query("\"hello world\"").unwrap();
|
||||
assert_eq!(q, SearchQuery::FullText("hello world".into()));
|
||||
}
|
||||
#[test]
|
||||
fn test_quoted() {
|
||||
let q = parse_search_query("\"hello world\"").unwrap();
|
||||
assert_eq!(q, SearchQuery::FullText("hello world".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_query_year() {
|
||||
let q = parse_search_query("year:2020..2023").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::RangeQuery {
|
||||
field: "year".into(),
|
||||
start: Some(2020),
|
||||
end: Some(2023)
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_range_query_year() {
|
||||
let q = parse_search_query("year:2020..2023").unwrap();
|
||||
assert_eq!(q, SearchQuery::RangeQuery {
|
||||
field: "year".into(),
|
||||
start: Some(2020),
|
||||
end: Some(2023),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_query_open_start() {
|
||||
let q = parse_search_query("year:..2023").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::RangeQuery {
|
||||
field: "year".into(),
|
||||
start: None,
|
||||
end: Some(2023)
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_range_query_open_start() {
|
||||
let q = parse_search_query("year:..2023").unwrap();
|
||||
assert_eq!(q, SearchQuery::RangeQuery {
|
||||
field: "year".into(),
|
||||
start: None,
|
||||
end: Some(2023),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_query_open_end() {
|
||||
let q = parse_search_query("year:2020..").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::RangeQuery {
|
||||
field: "year".into(),
|
||||
start: Some(2020),
|
||||
end: None
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_range_query_open_end() {
|
||||
let q = parse_search_query("year:2020..").unwrap();
|
||||
assert_eq!(q, SearchQuery::RangeQuery {
|
||||
field: "year".into(),
|
||||
start: Some(2020),
|
||||
end: None,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_greater_than() {
|
||||
let q = parse_search_query("year:>2020").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::CompareQuery {
|
||||
field: "year".into(),
|
||||
op: CompareOp::GreaterThan,
|
||||
value: 2020
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_compare_greater_than() {
|
||||
let q = parse_search_query("year:>2020").unwrap();
|
||||
assert_eq!(q, SearchQuery::CompareQuery {
|
||||
field: "year".into(),
|
||||
op: CompareOp::GreaterThan,
|
||||
value: 2020,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_less_or_equal() {
|
||||
let q = parse_search_query("year:<=2023").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::CompareQuery {
|
||||
field: "year".into(),
|
||||
op: CompareOp::LessOrEqual,
|
||||
value: 2023
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_compare_less_or_equal() {
|
||||
let q = parse_search_query("year:<=2023").unwrap();
|
||||
assert_eq!(q, SearchQuery::CompareQuery {
|
||||
field: "year".into(),
|
||||
op: CompareOp::LessOrEqual,
|
||||
value: 2023,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_size_compare_mb() {
|
||||
let q = parse_search_query("size:>10MB").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::CompareQuery {
|
||||
field: "size".into(),
|
||||
op: CompareOp::GreaterThan,
|
||||
value: 10 * 1024 * 1024
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_size_compare_mb() {
|
||||
let q = parse_search_query("size:>10MB").unwrap();
|
||||
assert_eq!(q, SearchQuery::CompareQuery {
|
||||
field: "size".into(),
|
||||
op: CompareOp::GreaterThan,
|
||||
value: 10 * 1024 * 1024,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_size_range_gb() {
|
||||
let q = parse_search_query("size:1GB..2GB").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::RangeQuery {
|
||||
field: "size".into(),
|
||||
start: Some(1024 * 1024 * 1024),
|
||||
end: Some(2 * 1024 * 1024 * 1024)
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_size_range_gb() {
|
||||
let q = parse_search_query("size:1GB..2GB").unwrap();
|
||||
assert_eq!(q, SearchQuery::RangeQuery {
|
||||
field: "size".into(),
|
||||
start: Some(1024 * 1024 * 1024),
|
||||
end: Some(2 * 1024 * 1024 * 1024),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_query_today() {
|
||||
let q = parse_search_query("created:today").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::DateQuery {
|
||||
field: "created".into(),
|
||||
value: DateValue::Today
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_date_query_today() {
|
||||
let q = parse_search_query("created:today").unwrap();
|
||||
assert_eq!(q, SearchQuery::DateQuery {
|
||||
field: "created".into(),
|
||||
value: DateValue::Today,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_query_last_week() {
|
||||
let q = parse_search_query("modified:last-week").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::DateQuery {
|
||||
field: "modified".into(),
|
||||
value: DateValue::LastWeek
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_date_query_last_week() {
|
||||
let q = parse_search_query("modified:last-week").unwrap();
|
||||
assert_eq!(q, SearchQuery::DateQuery {
|
||||
field: "modified".into(),
|
||||
value: DateValue::LastWeek,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_query_days_ago() {
|
||||
let q = parse_search_query("created:last-30d").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::DateQuery {
|
||||
field: "created".into(),
|
||||
value: DateValue::DaysAgo(30)
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_date_query_days_ago() {
|
||||
let q = parse_search_query("created:last-30d").unwrap();
|
||||
assert_eq!(q, SearchQuery::DateQuery {
|
||||
field: "created".into(),
|
||||
value: DateValue::DaysAgo(30),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue