shelly-data 0.3.0

Data-layer primitives for Shelly LiveView (schemas, changesets, repo, migrations).
Documentation
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()]
        );
    }
}