commit 895a304648cad059f9eb4e1f71c1899358286c86
parent b703354ca93644a5bc9117282018f74b41088ec0
Author: triesap <triesap@radroots.dev>
Date: Mon, 19 Jan 2026 07:26:47 +0000
app-utils: add model helper types
- add model query and filter types
- add model form and schema structs
- add query value helper functions
- add unit tests for model helpers
Diffstat:
2 files changed, 182 insertions(+), 0 deletions(-)
diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs
@@ -8,6 +8,7 @@ pub mod cache;
pub mod currency;
pub mod id;
pub mod media;
+pub mod model;
pub mod numbers;
pub mod object;
pub mod path;
@@ -29,6 +30,13 @@ pub use currency::{
};
pub use id::{d_tag_create, uuidv4, uuidv4_b64url, uuidv7, uuidv7_b64url};
pub use media::{fmt_media_image_upload_result_url, MediaImageUploadResult, MediaResource};
+pub use model::{
+ is_model_query_filter_option, is_model_query_filter_option_list, is_model_query_values,
+ list_model_query_values_assert, parse_model_query_value, ModelForm, ModelFormErrorTuple,
+ ModelFormValidationTuple, ModelQueryBindValue, ModelQueryBindValueOpt, ModelQueryBindValueTuple,
+ ModelQueryFilterCondition, ModelQueryFilterOption, ModelQueryFilterOptionList,
+ ModelQueryParam, ModelQueryValue, ModelSchemaErrors, ModelSortCreatedAt,
+};
pub use errors::{err_msg, handle_err, throw_err, ERR_PREFIX_APP, ERR_PREFIX_UTILS};
pub use numbers::{num_interval_range, num_str, parse_float, parse_int};
pub use object::{obj_en, obj_result, obj_results_str, obj_truthy_fields};
diff --git a/crates/utils/src/model/mod.rs b/crates/utils/src/model/mod.rs
@@ -0,0 +1,174 @@
+#![forbid(unsafe_code)]
+
+use crate::types::ValidationRegex;
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum ModelQueryValue {
+ String(String),
+ Number(f64),
+ Bool(bool),
+ Null,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum ModelQueryBindValue {
+ String(String),
+ Number(f64),
+ Null,
+}
+
+pub type ModelQueryBindValueTuple = (String, ModelQueryValue);
+pub type ModelQueryBindValueOpt = Option<ModelQueryBindValue>;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ModelQueryFilterOption {
+ Equals,
+ StartsWith,
+ EndsWith,
+ Contains,
+ NotEquals,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ModelQueryFilterOptionList {
+ Between,
+ In,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ModelQueryFilterCondition {
+ And,
+ Or,
+ Not,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ModelSortCreatedAt {
+ Newest,
+ Oldest,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct ModelQueryParam {
+ pub query: String,
+ pub bind_values: Vec<ModelQueryBindValue>,
+}
+
+pub type ModelFormErrorTuple = (bool, String);
+pub type ModelFormValidationTuple = (String, String);
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ModelSchemaErrors {
+ pub err_s: Vec<String>,
+}
+
+#[derive(Debug, Clone)]
+pub struct ModelForm {
+ pub label: Option<String>,
+ pub placeholder: Option<String>,
+ pub validate_keypress: Option<bool>,
+ pub prevent_focus_rest: Option<bool>,
+ pub hidden: Option<bool>,
+ pub optional: Option<bool>,
+ pub default: Option<ModelQueryValue>,
+ pub rxpv: ValidationRegex,
+}
+
+pub fn parse_model_query_value(value: &ModelQueryValue) -> ModelQueryBindValue {
+ match value {
+ ModelQueryValue::Bool(true) => ModelQueryBindValue::String("1".to_string()),
+ ModelQueryValue::Bool(false) => ModelQueryBindValue::String("0".to_string()),
+ ModelQueryValue::Number(value) => ModelQueryBindValue::Number(*value),
+ ModelQueryValue::String(value) => {
+ if value.is_empty() {
+ ModelQueryBindValue::Null
+ } else {
+ ModelQueryBindValue::String(value.clone())
+ }
+ }
+ ModelQueryValue::Null => ModelQueryBindValue::Null,
+ }
+}
+
+pub fn is_model_query_filter_option(value: &str) -> bool {
+ matches!(
+ value,
+ "equals" | "starts-with" | "ends-with" | "contains" | "ne"
+ )
+}
+
+pub fn is_model_query_filter_option_list(value: &str) -> bool {
+ matches!(value, "between" | "in")
+}
+
+pub fn is_model_query_values(value: &ModelQueryValue) -> bool {
+ !matches!(value, ModelQueryValue::Null)
+}
+
+pub fn list_model_query_values_assert(values: &[Option<ModelQueryValue>]) -> Vec<ModelQueryValue> {
+ values.iter().filter_map(|value| value.clone()).collect()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ is_model_query_filter_option, is_model_query_filter_option_list, is_model_query_values,
+ list_model_query_values_assert, parse_model_query_value, ModelQueryBindValue,
+ ModelQueryValue,
+ };
+
+ #[test]
+ fn parse_model_query_value_handles_bool() {
+ assert_eq!(
+ parse_model_query_value(&ModelQueryValue::Bool(true)),
+ ModelQueryBindValue::String("1".to_string())
+ );
+ assert_eq!(
+ parse_model_query_value(&ModelQueryValue::Bool(false)),
+ ModelQueryBindValue::String("0".to_string())
+ );
+ }
+
+ #[test]
+ fn parse_model_query_value_handles_string() {
+ assert_eq!(
+ parse_model_query_value(&ModelQueryValue::String("ok".to_string())),
+ ModelQueryBindValue::String("ok".to_string())
+ );
+ assert_eq!(
+ parse_model_query_value(&ModelQueryValue::String(String::new())),
+ ModelQueryBindValue::Null
+ );
+ }
+
+ #[test]
+ fn filter_option_checks() {
+ assert!(is_model_query_filter_option("equals"));
+ assert!(!is_model_query_filter_option("other"));
+ assert!(is_model_query_filter_option_list("between"));
+ assert!(!is_model_query_filter_option_list("other"));
+ }
+
+ #[test]
+ fn query_value_checks() {
+ assert!(is_model_query_values(&ModelQueryValue::String("ok".to_string())));
+ assert!(!is_model_query_values(&ModelQueryValue::Null));
+ }
+
+ #[test]
+ fn list_model_query_values_filters_none() {
+ let values = vec![
+ Some(ModelQueryValue::String("a".to_string())),
+ None,
+ Some(ModelQueryValue::Number(1.0)),
+ ];
+ let filtered = list_model_query_values_assert(&values);
+ assert_eq!(
+ filtered,
+ vec![
+ ModelQueryValue::String("a".to_string()),
+ ModelQueryValue::Number(1.0)
+ ]
+ );
+ }
+}