fraiseql_core/utils/operators.rs
1//! GraphQL WHERE clause operator registry.
2//!
3//! The registry is **derived** from [`fraiseql_db::where_clause::WHERE_OPERATORS`],
4//! which is the same list `WhereOperator::from_str` is generated from. Before
5//! #828 this file held a second, hand-maintained table of 79 names; 27 of them
6//! could not be parsed by the executor, so `GET /api/users?status[ne]=archived`
7//! passed REST validation and then failed with `Unknown WHERE operator: ne`, and
8//! the resulting 400 advertised two dozen more names that behaved the same way.
9//!
10//! Adding an operator now means adding one row to the table in `fraiseql-db`:
11//! the parser, this registry, and everything built on it move together.
12
13use std::{collections::HashMap, sync::LazyLock};
14
15pub use fraiseql_db::where_clause::{OperatorCategory, WHERE_OPERATORS, WhereOperatorSpec};
16
17/// Information about a single operator, as advertised to clients.
18#[derive(Debug, Clone)]
19pub struct OperatorInfo {
20 /// GraphQL operator name (e.g., "eq", "contains")
21 pub name: &'static str,
22 /// SQL operator or function (e.g., "=", "LIKE", "@>")
23 pub sql_op: &'static str,
24 /// Category of operator
25 pub category: OperatorCategory,
26 /// Whether this operator expects an array value
27 pub requires_array: bool,
28 /// Whether this operator needs special JSONB handling
29 pub jsonb_operator: bool,
30}
31
32impl From<&'static WhereOperatorSpec> for OperatorInfo {
33 fn from(spec: &'static WhereOperatorSpec) -> Self {
34 Self {
35 name: spec.name,
36 sql_op: spec.sql_op,
37 category: spec.category,
38 requires_array: spec.requires_array,
39 jsonb_operator: spec.jsonb_operator,
40 }
41 }
42}
43
44/// Every operator name the runtime accepts, canonical spellings and aliases
45/// alike, mapped to what that operator does.
46///
47/// Keyed by *every accepted* name, so a lookup and a parse agree: if the
48/// registry has the key, `WhereOperator::from_str` resolves it.
49pub static OPERATOR_REGISTRY: LazyLock<HashMap<&'static str, OperatorInfo>> = LazyLock::new(|| {
50 let mut m = HashMap::new();
51 for spec in WHERE_OPERATORS {
52 for name in spec.all_names() {
53 m.insert(name, OperatorInfo::from(spec));
54 }
55 }
56 m
57});
58
59/// Get operator information by name
60///
61/// # Example
62/// ```
63/// use fraiseql_core::utils::operators::get_operator_info;
64///
65/// let op = get_operator_info("eq").unwrap();
66/// assert_eq!(op.sql_op, "=");
67/// ```
68#[must_use]
69pub fn get_operator_info(name: &str) -> Option<&'static OperatorInfo> {
70 OPERATOR_REGISTRY.get(name)
71}
72
73/// Check if a string is a valid operator name
74///
75/// # Example
76/// ```
77/// use fraiseql_core::utils::operators::is_operator;
78///
79/// assert!(is_operator("eq"));
80/// assert!(is_operator("contains"));
81/// assert!(!is_operator("unknown_operator"));
82/// ```
83#[must_use]
84pub fn is_operator(name: &str) -> bool {
85 OPERATOR_REGISTRY.contains_key(name)
86}
87
88/// Get all operators in a specific category
89///
90/// # Example
91/// ```
92/// use fraiseql_core::utils::operators::{OperatorCategory, get_operators_by_category};
93///
94/// let comparison_ops = get_operators_by_category(OperatorCategory::Comparison);
95/// assert!(comparison_ops.len() >= 8);
96/// ```
97#[must_use]
98pub fn get_operators_by_category(category: OperatorCategory) -> Vec<&'static OperatorInfo> {
99 OPERATOR_REGISTRY.values().filter(|op| op.category == category).collect()
100}
101
102#[cfg(test)]
103#[path = "operators_tests.rs"]
104mod operators_tests;