use std::fmt;
use reqwest::Url;
#[derive(Default)]
pub enum QueryProfile {
#[default]
Minimal,
Full,
List,
}
impl fmt::Display for QueryProfile {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
QueryProfile::Minimal => write!(f, "minimal"),
QueryProfile::Full => write!(f, "full"),
QueryProfile::List => write!(f, "list"),
}
}
}
#[derive(Default)]
pub enum QueryPreset {
#[default]
Minimal,
Latest,
Analysis,
}
impl fmt::Display for QueryPreset {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
QueryPreset::Minimal => write!(f, "minimal"),
QueryPreset::Latest => write!(f, "latest"),
QueryPreset::Analysis => write!(f, "analysis"),
}
}
}
#[derive(Default, Debug, PartialEq)]
pub enum FilterOperator {
#[default]
OR,
AND,
}
impl fmt::Display for FilterOperator {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
FilterOperator::AND => write!(f, "AND"),
FilterOperator::OR => write!(f, "OR"),
}
}
}
#[derive(Default, Debug, PartialEq)]
pub struct QueryQuery {
pub value: String,
pub fields: Vec<String>,
pub operator: Option<FilterOperator>,
}
pub struct QueryFilter {
pub field: String,
pub value: String,
pub operator: Option<FilterOperator>,
pub negate: bool,
}
#[derive(Default)]
pub enum SortDirection {
#[default]
Asc,
Desc,
}
impl fmt::Display for SortDirection {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
SortDirection::Asc => write!(f, "asc"),
SortDirection::Desc => write!(f, "desc"),
}
}
}
pub struct SortDescriptor {
pub field: String,
pub direction: SortDirection,
}
#[derive(Default)]
pub struct QueryParams {
pub query: Option<QueryQuery>,
pub filter: Vec<QueryFilter>,
pub verbose: Option<bool>,
pub limit: Option<u32>,
pub offset: Option<u32>,
pub sort: Vec<SortDescriptor>,
pub profile: Option<QueryProfile>,
pub preset: Option<QueryPreset>,
pub include: Vec<String>,
pub exclude: Vec<String>,
}
impl QueryParams {
pub fn new() -> Self {
Self::default()
}
pub fn query(mut self, query: QueryQuery) -> Self {
self.query = Some(query);
self
}
pub fn filter(mut self, filter: QueryFilter) -> Self {
self.filter.push(filter);
self
}
pub fn filters(mut self, filters: Vec<QueryFilter>) -> Self {
self.filter.extend(filters);
self
}
pub fn verbose(mut self, v: bool) -> Self {
self.verbose = Some(v);
self
}
pub fn limit(mut self, l: u32) -> Self {
self.limit = Some(l);
self
}
pub fn offset(mut self, o: u32) -> Self {
self.offset = Some(o);
self
}
pub fn sort(mut self, sort: Vec<SortDescriptor>) -> Self {
self.sort.extend(sort);
self
}
pub fn profile(mut self, profile: QueryProfile) -> Self {
self.profile = Some(profile);
self
}
pub fn preset(mut self, preset: QueryPreset) -> Self {
self.preset = Some(preset);
self
}
pub fn include(mut self, include: Vec<String>) -> Self {
self.include.extend(include);
self
}
pub fn exclude(mut self, exclude: Vec<String>) -> Self {
self.exclude.extend(exclude);
self
}
}
impl QueryParams {
pub(crate) fn apply_to_url(&self, url: &mut Url) {
let mut qp = url.query_pairs_mut();
if let Some(v) = self.verbose {
qp.append_pair("verbose", if v { "1" } else { "0" });
}
if let Some(l) = self.limit {
qp.append_pair("limit", &l.to_string());
}
if let Some(o) = self.offset {
qp.append_pair("offset", &o.to_string());
}
if let Some(profile) = &self.profile {
qp.append_pair("profile", &profile.to_string());
}
if let Some(preset) = &self.preset {
qp.append_pair("preset", &preset.to_string());
}
for inc in &self.include {
qp.append_pair("fields[include][]", inc);
}
for exc in &self.exclude {
qp.append_pair("fields[exclude][]", exc);
}
if let Some(query) = &self.query {
qp.append_pair(&format!("query[value]"), &query.value);
for (j, field) in query.fields.iter().enumerate() {
qp.append_pair(&format!("query[fields][{j}]"), field);
}
if let Some(op) = &query.operator {
qp.append_pair(&format!("query[operator]"), &op.to_string());
}
}
if !self.filter.is_empty() {
if self.filter.iter().any(|f| f.operator.is_some()) {
let top_op = self
.filter
.iter()
.filter_map(|f| f.operator.as_ref())
.next()
.unwrap()
.to_string();
qp.append_pair("filter[operator]", &top_op);
}
for (i, f) in self.filter.iter().enumerate() {
qp.append_pair(&format!("filter[conditions][{i}][field]"), &f.field);
qp.append_pair(&format!("filter[conditions][{i}][value][]"), &f.value);
if f.negate {
qp.append_pair(&format!("filter[conditions][{i}][negate]"), "1");
}
if let Some(op) = &f.operator {
qp.append_pair(
&format!("filter[conditions][{i}][operator]"),
&op.to_string(),
);
}
}
}
for s in &self.sort {
qp.append_pair("sort[]", &format!("{}:{}", s.field, s.direction));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::Url;
#[test]
fn test_query_params_builder() {
let qp = QueryParams::new()
.verbose(true)
.limit(50)
.offset(10)
.profile(QueryProfile::Full)
.preset(QueryPreset::Analysis)
.include(vec!["field1".into(), "field2".into()])
.exclude(vec!["field3".into()])
.query(QueryQuery {
value: "search".into(),
fields: vec!["title".into()],
operator: Some(FilterOperator::AND),
})
.filter(QueryFilter {
field: "status".into(),
value: "active".into(),
operator: Some(FilterOperator::OR),
negate: false,
})
.sort(vec![SortDescriptor {
field: "date".into(),
direction: SortDirection::Desc,
}]);
assert_eq!(qp.verbose, Some(true));
assert_eq!(qp.limit, Some(50));
assert_eq!(qp.offset, Some(10));
assert_eq!(qp.profile.unwrap().to_string(), "full");
assert_eq!(qp.preset.unwrap().to_string(), "analysis");
assert_eq!(qp.include, vec!["field1", "field2"]);
assert_eq!(qp.exclude, vec!["field3"]);
assert_eq!(
qp.query,
Some(QueryQuery {
value: "search".to_string(),
fields: vec!["title".to_string()],
operator: Some(FilterOperator::AND)
})
);
assert_eq!(qp.filter.len(), 1);
assert_eq!(qp.sort.len(), 1);
}
#[test]
fn test_apply_to_url_basic() {
let mut url = Url::parse("https://example.com/api").unwrap();
let qp = QueryParams::new().verbose(true).limit(25).offset(5);
qp.apply_to_url(&mut url);
let query: Vec<(_, _)> = url.query_pairs().collect();
assert!(query.contains(&("verbose".into(), "1".into())));
assert!(query.contains(&("limit".into(), "25".into())));
assert!(query.contains(&("offset".into(), "5".into())));
}
#[test]
fn test_apply_to_url_include_exclude() {
let mut url = Url::parse("https://example.com/api").unwrap();
let qp = QueryParams::new()
.include(vec!["title".into(), "summary".into()])
.exclude(vec!["internal".into()]);
qp.apply_to_url(&mut url);
let query: Vec<(_, _)> = url.query_pairs().collect();
assert!(query.contains(&("fields[include][]".into(), "title".into())));
assert!(query.contains(&("fields[include][]".into(), "summary".into())));
assert!(query.contains(&("fields[exclude][]".into(), "internal".into())));
}
#[test]
fn test_apply_to_url_queries() {
let mut url = Url::parse("https://example.com/api").unwrap();
let qp = QueryParams::new().query(QueryQuery {
value: "foo".into(),
fields: vec!["title".into(), "content".into()],
operator: Some(FilterOperator::AND),
});
qp.apply_to_url(&mut url);
let query: Vec<(_, _)> = url.query_pairs().collect();
assert!(query.contains(&("query[value]".into(), "foo".into())));
assert!(query.contains(&("query[fields][0]".into(), "title".into())));
assert!(query.contains(&("query[fields][1]".into(), "content".into())));
assert!(query.contains(&("query[operator]".into(), "AND".into())));
}
#[test]
fn test_apply_to_url_filters() {
let mut url = Url::parse("https://example.com/api").unwrap();
let qp = QueryParams::new().filter(QueryFilter {
field: "status".into(),
value: "active".into(),
operator: Some(FilterOperator::OR),
negate: true,
});
qp.apply_to_url(&mut url);
let query: Vec<(_, _)> = url.query_pairs().collect();
assert!(query.contains(&("filter[operator]".into(), "OR".into())));
assert!(query.contains(&("filter[conditions][0][field]".into(), "status".into())));
assert!(query.contains(&("filter[conditions][0][value][]".into(), "active".into())));
assert!(query.contains(&("filter[conditions][0][negate]".into(), "1".into())));
}
#[test]
fn test_apply_to_url_sort() {
let mut url = Url::parse("https://example.com/api").unwrap();
let qp = QueryParams::new().sort(vec![SortDescriptor {
field: "date".into(),
direction: SortDirection::Desc,
}]);
qp.apply_to_url(&mut url);
let query: Vec<(_, _)> = url.query_pairs().collect();
assert!(query.contains(&("sort[]".into(), "date:desc".into())));
}
}