/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin */ use crate::{Filter, MultiBehaviour, Query, Sort, SortOrder, ValueSort}; use anyhow::{Error, anyhow, bail}; use jellyobject::{Path, Value}; use std::{fmt::Display, str::FromStr}; impl Display for Query<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if !matches!(self.filter, Filter::True) { write!(f, "FILTER {} ", self.filter)?; } if !matches!(self.sort, Sort::None) { write!(f, "SORT {} ", self.sort)?; } Ok(()) } } impl Display for Filter<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Filter::True => write!(f, "TRUE"), Filter::All(filters) => write!( f, "({})", filters .iter() .map(|f| f.to_string()) .collect::>() .join(" AND ") ), Filter::Any(filters) => write!( f, "({})", filters .iter() .map(|f| f.to_string()) .collect::>() .join(" OR ") ), Filter::Match(path, value) => { write!(f, "{path} = {value}") } Filter::Has(path) => write!(f, "{path}"), } } } impl Display for Sort { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Sort::None => write!(f, "NONE"), Sort::Random(0) => write!(f, "RANDOM"), Sort::Random(seed) => write!(f, "RANDOM WITH SEED {seed}"), Sort::Value(ValueSort { multi, order, path, .. }) => { write!( f, "{} BY {} {path}", match order { SortOrder::Ascending => "ASCENDING", SortOrder::Descending => "DESCENDING", }, match multi { MultiBehaviour::Count => "COUNT", MultiBehaviour::First => "FIRST", MultiBehaviour::ForEach => "EACH", MultiBehaviour::Max => "MAX", MultiBehaviour::Min => "MIN", }, ) } Sort::TextSearch(path, value) => { write!(f, "TEXT SEARCH {path} = {value:?}") } } } } impl FromStr for Query<'static> { type Err = Error; fn from_str(s: &str) -> Result { if let Some((filter, sort)) = s.split_once(" SORT ") && let Some(filter) = filter.strip_prefix("FILTER ") { Ok(Self { filter: Filter::from_str(filter)?, sort: Sort::from_str(sort)?, ..Default::default() }) } else if let Some(sort) = s.strip_prefix("SORT ") { Ok(Self { sort: Sort::from_str(sort)?, ..Default::default() }) } else if let Some(filter) = s.strip_prefix("FILTER") { Ok(Self { filter: Filter::from_str(filter)?, ..Default::default() }) } else { bail!("invalid query") } } } impl FromStr for Filter<'static> { type Err = Error; fn from_str(s: &str) -> Result { Ok(match s { "TRUE" => Self::True, x if let Some(x) = x.strip_prefix("(") && let Some(x) = x.strip_suffix(")") && let Some((l, r)) = x.split_once("AND") => { Self::All(vec![Filter::from_str(l)?, Filter::from_str(r)?]) } x if let Some(x) = x.strip_prefix("(") && let Some(x) = x.strip_suffix(")") && let Some((l, r)) = x.split_once("OR") => { Self::Any(vec![ Filter::from_str(l.trim())?, Filter::from_str(r.trim())?, ]) } x if let Some((l, r)) = x.split_once("=") => Self::Match( Path::from_str(l.trim()).map_err(|e| anyhow!("{e}"))?, Value::from_str(r.trim()).map_err(|e| anyhow!("{e}"))?, ), x => Self::Has(Path::from_str(x.trim()).map_err(|e| anyhow!("{e}"))?), }) } } impl FromStr for Sort { type Err = Error; fn from_str(s: &str) -> Result { Ok(if s == "NONE" { Sort::None } else if s == "RANDOM" { Sort::Random(1) } else if let Some(s) = s.strip_prefix("TEXT SEARCH ") && let Some((path, value)) = s.split_once(" = ") { Sort::TextSearch( Path::from_str(path).map_err(|e| anyhow!("{e}"))?, value.to_owned(), ) } else if let Some((order, rest)) = s.split_once(" BY ") && let Some((multi, path)) = rest.split_once(" ") { Sort::Value(ValueSort { order: match order { "ASCENDING" => SortOrder::Ascending, "DESCENDING" => SortOrder::Descending, _ => bail!("unknown order"), }, multi: match multi { "COUNT" => MultiBehaviour::Count, "FIRST" => MultiBehaviour::First, "EACH" => MultiBehaviour::ForEach, "MAX" => MultiBehaviour::Max, "MIN" => MultiBehaviour::Min, _ => bail!("unknown multi bahav"), }, path: Path::from_str(path).map_err(|e| anyhow!("{e}"))?, offset: None, }) } else { bail!("unknown sort") }) } } #[test] fn test_parse() { use jellyobject::Tag; assert_eq!( Query::from_str("FILTER (visi = visi AND kind = vide) SORT DESCENDING BY FIRST rldt") .unwrap(), Query { filter: Filter::All(vec![ Filter::Match(Path(vec![Tag::new(b"visi")]), Tag::new(b"visi").into()), Filter::Match(Path(vec![Tag::new(b"kind")]), Tag::new(b"vide").into()), ]), sort: Sort::Value(ValueSort { order: SortOrder::Descending, path: Path(vec![Tag::new(b"rldt")]), multi: MultiBehaviour::First, offset: None, }), ..Default::default() } ) }