use crate::filter::Filter;
use crate::sort::Sort;
use crate::{
CursorDirection, CursorParams, FilterOperator, FilterParams, FilterValue,
IntoParams, SearchParams, SortingParams, params::Params,
};
use std::marker::PhantomData;
#[derive(Debug)]
pub struct ParamsBuilder {
params: Params,
}
impl Default for ParamsBuilder {
fn default() -> Self {
Self::new()
}
}
impl ParamsBuilder {
pub fn new() -> Self {
Self {
params: Params::default(),
}
}
pub fn sort(self) -> SortBuilder<Self, Safe> {
SortBuilder::with_parent(self)
}
pub fn filter(self) -> FilterBuilder<Self> {
FilterBuilder::with_parent(self)
}
pub fn search(self) -> SearchBuilder<Self> {
SearchBuilder::with_parent(self)
}
pub fn serial(self) -> SerialBuilder<Self> {
SerialBuilder::with_parent(self)
}
pub fn slice(self) -> SliceBuilder<Self> {
SliceBuilder::with_parent(self)
}
pub fn cursor(self) -> CursorBuilder<Self, Initial> {
CursorBuilder::with_parent(self)
}
pub fn limit(mut self, limit: u32) -> Self {
self.params.limit = Some(crate::pagination::LimitParam(limit));
self
}
pub fn offset(mut self, offset: u32) -> Self {
self.params.offset = Some(crate::pagination::OffsetParam(offset));
self
}
pub fn build(self) -> Params {
self.params
}
}
pub struct Safe;
pub struct Unsafe;
pub struct SortBuilder<P = (), S = Safe> {
parent: Option<P>,
sorts: SortingParams,
allowed_columns: Option<&'static [&'static str]>,
_security: PhantomData<S>,
}
impl Default for SortBuilder<(), Safe> {
fn default() -> Self {
Self::new()
}
}
impl<S> std::fmt::Debug for SortBuilder<(), S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SortBuilder")
.field("sorts", &self.sorts)
.field("allowed_columns", &self.allowed_columns)
.finish()
}
}
impl SortBuilder<(), Safe> {
pub fn new() -> Self {
Self {
parent: None,
sorts: SortingParams::new(),
allowed_columns: None,
_security: PhantomData,
}
}
pub fn build(self) -> Option<SortingParams> {
if self.sorts.is_empty() {
None
} else {
Some(self.sorts)
}
}
}
impl<P> SortBuilder<P, Safe> {
fn with_parent(parent: P) -> Self {
Self {
parent: Some(parent),
sorts: SortingParams::new(),
allowed_columns: None,
_security: PhantomData,
}
}
pub fn asc(mut self, field: &'static str) -> Self {
self.sorts = self.sorts.asc(field);
self
}
pub fn desc(mut self, field: &'static str) -> Self {
self.sorts = self.sorts.desc(field);
self
}
pub fn with_allowed_columns(self, allowed_columns: &'static [&'static str]) -> SortBuilder<P, Unsafe> {
SortBuilder {
parent: self.parent,
sorts: self.sorts.with_allowed_columns(allowed_columns),
allowed_columns: Some(allowed_columns),
_security: PhantomData,
}
}
}
impl SortBuilder<(), Unsafe> {
pub fn build(self) -> Option<SortingParams> {
if self.sorts.is_empty() {
None
} else {
Some(self.sorts)
}
}
}
impl<P> SortBuilder<P, Unsafe> {
pub fn asc_unsafe(mut self, field: impl Into<String>) -> Self {
let sort = Sort::asc_unsafe(field.into());
self.sorts = self.sorts.push(sort);
self
}
pub fn desc_unsafe(mut self, field: impl Into<String>) -> Self {
let sort = Sort::desc_unsafe(field.into());
self.sorts = self.sorts.push(sort);
self
}
}
impl<P, S> SortBuilder<P, S> {
pub fn nulls_first(mut self) -> Self {
self.sorts = self.sorts.apply_nulls_first();
self
}
pub fn nulls_last(mut self) -> Self {
self.sorts = self.sorts.apply_nulls_last();
self
}
pub fn nulls_default(mut self) -> Self {
self.sorts = self.sorts.apply_nulls_default();
self
}
pub fn done(self) -> P
where
P: HasParams,
{
#[allow(clippy::expect_used)]
let mut parent = self
.parent
.expect("SortBuilder::done called without a parent (programming error)");
if !self.sorts.is_empty() {
if let Some(existing_sorts) = parent.params_mut().sort_by.take() {
parent.params_mut().sort_by = Some(existing_sorts.extend_with(self.sorts));
} else {
parent.params_mut().sort_by = Some(self.sorts);
}
}
parent
}
}
pub struct FilterBuilder<P = ()> {
parent: Option<P>,
filters: FilterParams,
}
impl Default for FilterBuilder<()> {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for FilterBuilder<()> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FilterBuilder")
.field("filters", &self.filters)
.finish()
}
}
impl FilterBuilder<()> {
pub fn new() -> Self {
Self {
parent: None,
filters: FilterParams::default(),
}
}
pub fn build(self) -> FilterParams {
self.filters
}
}
impl<P> FilterBuilder<P> {
pub fn with_parent(parent: P) -> Self {
Self {
parent: Some(parent),
filters: FilterParams::default(),
}
}
fn push(
mut self,
field: impl Into<String>,
op: FilterOperator,
value: impl Into<FilterValue>,
) -> Self {
self.filters.filters.push(Filter::new(field, op, value));
self
}
pub fn eq(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
self.push(field, FilterOperator::Eq, value)
}
pub fn ne(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
self.push(field, FilterOperator::Ne, value)
}
pub fn gt(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
self.push(field, FilterOperator::Gt, value)
}
pub fn lt(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
self.push(field, FilterOperator::Lt, value)
}
pub fn gte(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
self.push(field, FilterOperator::Gte, value)
}
pub fn lte(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
self.push(field, FilterOperator::Lte, value)
}
pub fn like(self, field: impl Into<String>, pat: impl Into<String>) -> Self {
self.push(field, FilterOperator::Like, pat.into())
}
pub fn ilike(self, field: impl Into<String>, pat: impl Into<String>) -> Self {
self.push(field, FilterOperator::ILike, pat.into())
}
pub fn like_pattern(self, field: impl Into<String>, pat: impl Into<String>) -> Self {
self.push(field, FilterOperator::UnsafeLike, pat.into())
}
pub fn r#in(
self,
field: impl Into<String>,
values: impl IntoIterator<Item = impl Into<FilterValue>>,
) -> Self {
let array = FilterValue::Array(values.into_iter().map(Into::into).collect());
self.push(field, FilterOperator::In, array)
}
pub fn in_values(
self,
field: impl Into<String>,
values: impl IntoIterator<Item = impl Into<FilterValue>>,
) -> Self {
self.r#in(field, values)
}
pub fn not_in(
self,
field: impl Into<String>,
values: impl IntoIterator<Item = impl Into<FilterValue>>,
) -> Self {
self.push(
field,
FilterOperator::NotIn,
FilterValue::Array(values.into_iter().map(Into::into).collect()),
)
}
pub fn between(
self,
field: impl Into<String>,
min: impl Into<FilterValue>,
max: impl Into<FilterValue>,
) -> Self {
self.push(
field,
FilterOperator::Between,
FilterValue::Array(vec![min.into(), max.into()]),
)
}
pub fn is_null(self, field: impl Into<String>) -> Self {
self.push(field, FilterOperator::IsNull, FilterValue::Null)
}
pub fn is_not_null(self, field: impl Into<String>) -> Self {
self.push(field, FilterOperator::IsNotNull, FilterValue::Null)
}
pub fn contains(self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
self.push(field, FilterOperator::Contains, value)
}
#[allow(clippy::should_implement_trait)]
pub fn not(mut self) -> Self {
if let Some(last_filter) = self.filters.filters.last_mut() {
last_filter.not = true;
}
self
}
pub fn done(self) -> P
where
P: HasParams,
{
#[allow(clippy::expect_used)]
let mut parent = self
.parent
.expect("FilterBuilder::done called without a parent (programming error)");
if let Some(ref mut existing_filters) = parent.params_mut().filters {
existing_filters.filters.extend(self.filters.filters);
} else {
parent.params_mut().filters = Some(self.filters);
}
parent
}
}
pub struct SearchBuilder<P = ()> {
parent: Option<P>,
search: SearchParams,
}
impl Default for SearchBuilder<()> {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for SearchBuilder<()> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SearchBuilder")
.field("search", &self.search)
.finish()
}
}
impl SearchBuilder<()> {
pub fn new() -> Self {
Self {
parent: None,
search: SearchParams::new("", vec![]),
}
}
pub fn build(self) -> SearchParams {
self.search
}
}
impl<P> SearchBuilder<P> {
pub fn with_parent(parent: P) -> Self {
Self {
parent: Some(parent),
search: SearchParams::new("", vec![]),
}
}
pub fn query(mut self, q: impl Into<String>) -> Self {
self.search.query = q.into();
self
}
pub fn search<I, S>(mut self, query: impl Into<String>, fields: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.search.query = query.into();
self.search.fields = fields.into_iter().map(|s| s.into()).collect();
self
}
pub fn fields<I, S>(mut self, fields: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.search.fields = fields.into_iter().map(Into::into).collect();
self
}
pub fn exact(mut self, yes: bool) -> Self {
self.search = self.search.with_exact_match(yes);
self
}
pub fn case_sensitive(mut self, yes: bool) -> Self {
self.search = self.search.with_case_sensitive(yes);
self
}
pub fn done(self) -> P
where
P: HasParams,
{
#[allow(clippy::expect_used)]
let mut parent = self
.parent
.expect("SearchBuilder::done called without a parent (programming error)");
if !self.search.query.is_empty() {
parent.params_mut().search = Some(self.search);
}
parent
}
}
pub struct SerialBuilder<P = ()> {
parent: Option<P>,
serial: crate::serial::SerialParams,
}
impl Default for SerialBuilder<()> {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for SerialBuilder<()> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SerialBuilder")
.field("serial", &self.serial)
.finish()
}
}
impl SerialBuilder<()> {
pub fn new() -> Self {
Self {
parent: None,
serial: crate::serial::SerialParams::default(),
}
}
pub fn with_page(page: u32, per_page: u32) -> Self {
Self {
parent: None,
serial: crate::serial::SerialParams::new(page, per_page),
}
}
pub fn build(self) -> crate::serial::SerialParams {
self.serial
}
}
impl<P> SerialBuilder<P> {
pub fn with_parent(parent: P) -> Self {
Self {
parent: Some(parent),
serial: crate::serial::SerialParams::default(),
}
}
pub fn page(mut self, page: u32, per_page: u32) -> Self {
self.serial = crate::serial::SerialParams::new(page, per_page);
self
}
pub fn done(self) -> P
where
P: HasParams,
{
#[allow(clippy::expect_used)]
let mut parent = self
.parent
.expect("SerialBuilder::done called without a parent (programming error)");
let params = parent.params_mut();
let limit = self.serial.limit();
let offset = self.serial.offset();
params.pagination = Some(crate::pagination::Pagination::Serial(self.serial));
params.limit = Some(crate::pagination::LimitParam(limit));
params.offset = Some(crate::pagination::OffsetParam(offset));
parent
}
}
pub struct SliceBuilder<P = ()> {
parent: Option<P>,
slice: crate::slice::SliceParams,
}
impl Default for SliceBuilder<()> {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for SliceBuilder<()> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SliceBuilder")
.field("slice", &self.slice)
.finish()
}
}
impl SliceBuilder<()> {
pub fn new() -> Self {
Self {
parent: None,
slice: crate::slice::SliceParams::default(),
}
}
pub fn with_page(page: u32, per_page: u32) -> Self {
Self {
parent: None,
slice: crate::slice::SliceParams::new(page, per_page),
}
}
pub fn build(self) -> crate::slice::SliceParams {
self.slice
}
}
impl<P> SliceBuilder<P> {
pub fn with_parent(parent: P) -> Self {
Self {
parent: Some(parent),
slice: crate::slice::SliceParams::default(),
}
}
pub fn page(mut self, page: u32, per_page: u32) -> Self {
self.slice = crate::slice::SliceParams::new(page, per_page);
self
}
pub fn enable_total_count(mut self) -> Self {
self.slice = self.slice.with_disable_total_count(false);
self
}
pub fn done(self) -> P
where
P: HasParams,
{
#[allow(clippy::expect_used)]
let mut parent = self
.parent
.expect("SliceBuilder::done called without a parent (programming error)");
let params = parent.params_mut();
let limit = self.slice.limit();
let offset = self.slice.offset();
params.pagination = Some(crate::pagination::Pagination::Slice(self.slice));
params.limit = Some(crate::pagination::LimitParam(limit));
params.offset = Some(crate::pagination::OffsetParam(offset));
parent
}
}
pub struct Initial;
pub struct FirstPage;
pub struct After;
pub struct Before;
pub struct CursorBuilder<P = (), S = Initial> {
parent: Option<P>,
cursor: CursorParams,
_state: PhantomData<S>,
}
impl Default for CursorBuilder<(), Initial> {
fn default() -> Self {
Self::new()
}
}
impl<S> std::fmt::Debug for CursorBuilder<(), S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CursorBuilder")
.field("cursor", &self.cursor)
.finish()
}
}
impl CursorBuilder<(), Initial> {
pub fn new() -> Self {
Self {
parent: None,
cursor: CursorParams::default(),
_state: PhantomData,
}
}
pub fn build(self) -> CursorParams {
self.cursor
}
}
impl<P> CursorBuilder<P, Initial> {
pub fn with_parent(parent: P) -> Self {
Self {
parent: Some(parent),
cursor: CursorParams::default(),
_state: PhantomData,
}
}
pub fn first_page(self) -> CursorBuilder<P, FirstPage> {
CursorBuilder {
parent: self.parent,
cursor: CursorParams::default(), _state: PhantomData,
}
}
pub fn after(
self,
value: impl Into<FilterValue>
) -> CursorBuilder<P, After> {
let mut cursor = CursorParams::default();
cursor = cursor.and_field(value.into());
cursor.direction = Some(CursorDirection::After);
CursorBuilder {
parent: self.parent,
cursor,
_state: PhantomData,
}
}
pub fn before(
self,
value: impl Into<FilterValue>
) -> CursorBuilder<P, Before> {
let mut cursor = CursorParams::default();
cursor = cursor.and_field(value.into());
cursor.direction = Some(CursorDirection::Before);
CursorBuilder {
parent: self.parent,
cursor,
_state: PhantomData,
}
}
pub fn next_cursor<T: crate::CursorSecureExtract>(self, token: impl Into<String>) -> CursorBuilder<P, After> {
self.decoded::<T, After>(token, CursorDirection::After)
}
pub fn prev_cursor<T: crate::CursorSecureExtract>(self, token: impl Into<String>) -> CursorBuilder<P, Before> {
self.decoded::<T, Before>(token, CursorDirection::Before)
}
fn decoded<T: crate::CursorSecureExtract, S>(
self,
token: impl Into<String>,
direction: CursorDirection,
) -> CursorBuilder<P, S> {
let token = token.into();
let cursor = match T::decode(&token) {
Ok(decoded_values) => {
CursorParams::from_values(decoded_values, direction)
}
Err(e) => {
CursorParams::with_error(direction, e.to_string())
}
};
CursorBuilder {
parent: self.parent,
cursor,
_state: PhantomData,
}
}
}
impl<P> CursorBuilder<P, FirstPage> {
pub fn done(self) -> P
where
P: HasParams,
{
#[allow(clippy::expect_used)]
let mut parent = self
.parent
.expect("CursorBuilder::done called without a parent (programming error)");
let params = parent.params_mut();
params.pagination = Some(crate::pagination::Pagination::Cursor(self.cursor));
parent
}
}
impl<P> CursorBuilder<P, After> {
pub fn and_field(mut self, value: impl Into<FilterValue>) -> Self {
self.cursor = self.cursor.and_field(value.into());
self
}
pub fn done(self) -> P
where
P: HasParams,
{
#[allow(clippy::expect_used)]
let mut parent = self
.parent
.expect("CursorBuilder::done called without a parent (programming error)");
let params = parent.params_mut();
params.pagination = Some(crate::pagination::Pagination::Cursor(self.cursor));
parent
}
}
impl<P> CursorBuilder<P, Before> {
pub fn and_field(mut self, value: impl Into<FilterValue>) -> Self {
self.cursor = self.cursor.and_field(value.into());
self
}
pub fn done(self) -> P
where
P: HasParams,
{
#[allow(clippy::expect_used)]
let mut parent = self
.parent
.expect("CursorBuilder::done called without a parent (programming error)");
let params = parent.params_mut();
params.pagination = Some(crate::pagination::Pagination::Cursor(self.cursor));
parent
}
}
pub trait HasParams {
fn params_mut(&mut self) -> &mut Params;
}
impl HasParams for ParamsBuilder {
fn params_mut(&mut self) -> &mut Params {
&mut self.params
}
}
impl IntoParams for ParamsBuilder {
fn into_params(self) -> Params {
self.params
}
}
impl IntoParams for FilterBuilder<()> {
fn into_params(self) -> Params {
Params {
filters: if self.filters.filters.is_empty() {
None
} else {
Some(self.filters)
},
..Default::default()
}
}
}
impl IntoParams for SearchBuilder<()> {
fn into_params(self) -> Params {
Params {
search: if self.search.query.is_empty() {
None
} else {
Some(self.search)
},
..Default::default()
}
}
}
impl IntoParams for SerialBuilder<()> {
fn into_params(self) -> Params {
let limit = self.serial.limit();
let offset = self.serial.offset();
Params {
pagination: Some(crate::pagination::Pagination::Serial(self.serial)),
limit: Some(crate::pagination::LimitParam(limit)),
offset: Some(crate::pagination::OffsetParam(offset)),
..Default::default()
}
}
}
impl IntoParams for SliceBuilder<()> {
fn into_params(self) -> Params {
let limit = self.slice.limit();
let offset = self.slice.offset();
Params {
pagination: Some(crate::pagination::Pagination::Slice(self.slice)),
limit: Some(crate::pagination::LimitParam(limit)),
offset: Some(crate::pagination::OffsetParam(offset)),
..Default::default()
}
}
}
impl IntoParams for CursorBuilder<(), Initial> {
fn into_params(self) -> Params {
Params {
..Default::default()
}
}
}
impl IntoParams for CursorBuilder<(), FirstPage> {
fn into_params(self) -> Params {
Params {
pagination: Some(crate::pagination::Pagination::Cursor(self.cursor)),
..Default::default()
}
}
}
impl IntoParams for CursorBuilder<(), After> {
fn into_params(self) -> Params {
Params {
pagination: Some(crate::pagination::Pagination::Cursor(self.cursor)),
..Default::default()
}
}
}
impl IntoParams for CursorBuilder<(), Before> {
fn into_params(self) -> Params {
Params {
pagination: Some(crate::pagination::Pagination::Cursor(self.cursor)),
..Default::default()
}
}
}
impl<S> IntoParams for SortBuilder<(), S> {
fn into_params(self) -> Params {
Params {
sort_by: if self.sorts.is_empty() {
None
} else {
Some(self.sorts)
},
..Default::default()
}
}
}
#[cfg(test)]
mod security_tests {
use super::*;
#[test]
fn test_safe_sort_builder_compile_time_safety() {
let params = ParamsBuilder::new()
.sort()
.asc("id") .desc("created_at") .done()
.build();
assert!(params.sort_by.is_some());
let sort_by = params.sort_by.unwrap();
assert_eq!(sort_by.sorts().len(), 2);
assert_eq!(sort_by.sorts()[0].field, "id");
assert!(sort_by.sorts()[0].is_asc());
assert_eq!(sort_by.sorts()[1].field, "created_at");
assert!(!sort_by.sorts()[1].is_asc());
}
#[test]
fn test_unsafe_sort_builder_with_runtime_validation() {
let dynamic_field = "name".to_string();
let invalid_field = "malicious_field".to_string();
let params = ParamsBuilder::new()
.sort()
.with_allowed_columns(&["id", "name", "created_at", "age"])
.asc_unsafe(dynamic_field) .desc_unsafe(invalid_field) .done()
.build();
assert!(params.sort_by.is_some());
let sort_by = params.sort_by.unwrap();
assert_eq!(sort_by.sorts().len(), 2);
assert_eq!(sort_by.sorts()[0].field, "name");
assert!(sort_by.sorts()[0].is_asc());
assert_eq!(sort_by.sorts()[1].field, "malicious_field");
assert!(!sort_by.sorts()[1].is_asc());
assert!(sort_by.has_unsafe_fields());
assert!(sort_by.validate_fields().is_err());
}
#[test]
fn test_standalone_sort_builder() {
let safe_sort = SortBuilder::new()
.asc("id")
.desc("created_at")
.build();
assert!(safe_sort.is_some());
let sort_params = safe_sort.unwrap();
assert_eq!(sort_params.sorts().len(), 2);
let unsafe_sort = SortBuilder::new()
.with_allowed_columns(&["id", "name"])
.asc_unsafe("invalid_field".to_string()) .build();
assert!(unsafe_sort.is_some());
let sort_params = unsafe_sort.unwrap();
assert_eq!(sort_params.sorts().len(), 1);
assert_eq!(sort_params.sorts()[0].field, "invalid_field"); assert!(sort_params.has_unsafe_fields());
assert!(sort_params.validate_fields().is_err());
}
#[test]
fn test_runtime_validation_success() {
let params = ParamsBuilder::new()
.sort()
.with_allowed_columns(&["id", "name", "email"])
.asc_unsafe("name".to_string()) .desc_unsafe("id".to_string()) .done()
.build();
let sort_by = params.sort_by.unwrap();
assert!(sort_by.has_unsafe_fields());
assert!(sort_by.validate_fields().is_ok());
}
}