use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FilterOperator {
Eq,
Neq,
Gt,
Gte,
Lt,
Lte,
Contains,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Filter {
pub field: String,
pub op: FilterOperator,
pub value: Value,
}
impl Filter {
pub fn eq(field: impl Into<String>, value: Value) -> Self {
Self {
field: field.into(),
op: FilterOperator::Eq,
value,
}
}
pub fn contains(field: impl Into<String>, value: impl Into<String>) -> Self {
Self {
field: field.into(),
op: FilterOperator::Contains,
value: Value::String(value.into()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SortDirection {
Asc,
Desc,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Sort {
pub field: String,
pub direction: SortDirection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pagination {
pub page: usize,
pub per_page: usize,
}
impl Pagination {
pub fn new(page: usize, per_page: usize) -> Self {
Self {
page: page.max(1),
per_page: per_page.max(1),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Query {
pub filters: Vec<Filter>,
pub sorts: Vec<Sort>,
pub pagination: Option<Pagination>,
pub preloads: Vec<String>,
}
impl Query {
pub fn new() -> Self {
Self::default()
}
pub fn where_filter(mut self, filter: Filter) -> Self {
self.filters.push(filter);
self
}
pub fn order_by(mut self, field: impl Into<String>, direction: SortDirection) -> Self {
self.sorts.push(Sort {
field: field.into(),
direction,
});
self
}
pub fn paginate(mut self, page: usize, per_page: usize) -> Self {
self.pagination = Some(Pagination::new(page, per_page));
self
}
pub fn preload(mut self, relation: impl Into<String>) -> Self {
self.preloads.push(relation.into());
self
}
}
#[cfg(test)]
mod tests {
use super::{Filter, FilterOperator, Pagination, Query, SortDirection};
use serde_json::json;
#[test]
fn pagination_clamps_zero_values_to_one() {
let pagination = Pagination::new(0, 0);
assert_eq!(pagination.page, 1);
assert_eq!(pagination.per_page, 1);
}
#[test]
fn filter_builders_create_expected_filters() {
assert_eq!(
Filter::eq("title", json!("Hello")),
Filter {
field: "title".to_string(),
op: FilterOperator::Eq,
value: json!("Hello"),
}
);
assert_eq!(
Filter::contains("body", "rust"),
Filter {
field: "body".to_string(),
op: FilterOperator::Contains,
value: json!("rust"),
}
);
}
#[test]
fn query_builder_collects_filters_sorts_pagination_and_preloads() {
let query = Query::new()
.where_filter(Filter::eq("id", json!(10)))
.where_filter(Filter::contains("title", "post"))
.order_by("inserted_at", SortDirection::Desc)
.order_by("title", SortDirection::Asc)
.paginate(0, 50)
.preload("author")
.preload("comments");
assert_eq!(query.filters.len(), 2);
assert_eq!(query.sorts.len(), 2);
assert_eq!(
query.pagination,
Some(Pagination {
page: 1,
per_page: 50
})
);
assert_eq!(
query.preloads,
vec!["author".to_string(), "comments".to_string()]
);
}
}