Skip to main content

vantage_api_client/graphql/
operation.rs

1//! `GraphqlOperation` — typed comparison/logical operators that produce
2//! `GraphqlCondition`. Blanket-implemented over all `Expressive<T>`, so
3//! typed columns get `.eq()`/`.gt()`/`.in_()`/… for free.
4//!
5//! The field name is the operand's expression template, which is the column
6//! name for a typed `Column<T>`. A complex expression lands its rendered
7//! template as the field, so keep operands to plain columns.
8
9use serde_json::Value;
10use vantage_expressions::Expressive;
11
12use crate::graphql::condition::{FieldCondition, GraphqlCondition, GraphqlOp};
13use crate::graphql::types::{AnyGraphqlType, GraphqlType};
14
15fn field_name<T>(expr: &(impl Expressive<T> + ?Sized)) -> String {
16    expr.expr().template.clone()
17}
18
19fn to_json(value: impl Into<AnyGraphqlType>) -> Value {
20    value.into().to_json()
21}
22
23fn to_json_array<I, V>(values: I) -> Value
24where
25    I: IntoIterator<Item = V>,
26    V: Into<AnyGraphqlType>,
27{
28    Value::Array(values.into_iter().map(to_json).collect())
29}
30
31/// GraphQL operators that produce a [`GraphqlCondition`].
32///
33/// Blanket-impl'd for any `Expressive<T>`. Import this trait alongside
34/// your column types when writing GraphQL filters; don't mix it with
35/// `vantage_table::operation::Operation` in the same scope (which would
36/// shadow these methods with raw-expression versions).
37pub trait GraphqlOperation<T>: Expressive<T> {
38    /// `field = value`
39    fn eq(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
40    where
41        Self: Sized,
42    {
43        GraphqlCondition::Field(FieldCondition::new(
44            field_name(self),
45            GraphqlOp::Eq,
46            to_json(value),
47        ))
48    }
49
50    /// `field != value`
51    fn ne(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
52    where
53        Self: Sized,
54    {
55        GraphqlCondition::Field(FieldCondition::new(
56            field_name(self),
57            GraphqlOp::Ne,
58            to_json(value),
59        ))
60    }
61
62    /// `field > value`
63    fn gt(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
64    where
65        Self: Sized,
66    {
67        GraphqlCondition::Field(FieldCondition::new(
68            field_name(self),
69            GraphqlOp::Gt,
70            to_json(value),
71        ))
72    }
73
74    /// `field >= value`
75    fn gte(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
76    where
77        Self: Sized,
78    {
79        GraphqlCondition::Field(FieldCondition::new(
80            field_name(self),
81            GraphqlOp::Gte,
82            to_json(value),
83        ))
84    }
85
86    /// `field < value`
87    fn lt(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
88    where
89        Self: Sized,
90    {
91        GraphqlCondition::Field(FieldCondition::new(
92            field_name(self),
93            GraphqlOp::Lt,
94            to_json(value),
95        ))
96    }
97
98    /// `field <= value`
99    fn lte(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
100    where
101        Self: Sized,
102    {
103        GraphqlCondition::Field(FieldCondition::new(
104            field_name(self),
105            GraphqlOp::Lte,
106            to_json(value),
107        ))
108    }
109
110    /// `field IN [values...]`
111    fn in_<I, V>(&self, values: I) -> GraphqlCondition
112    where
113        Self: Sized,
114        I: IntoIterator<Item = V>,
115        V: Into<AnyGraphqlType>,
116    {
117        GraphqlCondition::Field(FieldCondition::new(
118            field_name(self),
119            GraphqlOp::In,
120            to_json_array(values),
121        ))
122    }
123
124    /// `field NOT IN [values...]`
125    fn not_in<I, V>(&self, values: I) -> GraphqlCondition
126    where
127        Self: Sized,
128        I: IntoIterator<Item = V>,
129        V: Into<AnyGraphqlType>,
130    {
131        GraphqlCondition::Field(FieldCondition::new(
132            field_name(self),
133            GraphqlOp::NotIn,
134            to_json_array(values),
135        ))
136    }
137
138    /// `field LIKE pattern` — case-sensitive substring match.
139    fn like(&self, pattern: impl Into<String>) -> GraphqlCondition
140    where
141        Self: Sized,
142    {
143        GraphqlCondition::Field(FieldCondition::new(
144            field_name(self),
145            GraphqlOp::Like,
146            Value::String(pattern.into()),
147        ))
148    }
149
150    /// `field ILIKE pattern` — case-insensitive substring match.
151    fn ilike(&self, pattern: impl Into<String>) -> GraphqlCondition
152    where
153        Self: Sized,
154    {
155        GraphqlCondition::Field(FieldCondition::new(
156            field_name(self),
157            GraphqlOp::ILike,
158            Value::String(pattern.into()),
159        ))
160    }
161
162    /// `field IS NULL`
163    fn is_null(&self) -> GraphqlCondition
164    where
165        Self: Sized,
166    {
167        GraphqlCondition::Field(FieldCondition::new(
168            field_name(self),
169            GraphqlOp::IsNull,
170            Value::Null,
171        ))
172    }
173
174    /// `field IS NOT NULL`
175    fn is_not_null(&self) -> GraphqlCondition
176    where
177        Self: Sized,
178    {
179        GraphqlCondition::Field(FieldCondition::new(
180            field_name(self),
181            GraphqlOp::IsNotNull,
182            Value::Null,
183        ))
184    }
185}
186
187/// Blanket: any `Expressive<T>` gets `GraphqlOperation<T>` for free.
188impl<T, S: Expressive<T>> GraphqlOperation<T> for S {}
189
190// Keeps the `GraphqlType` import used: `to_json` needs the trait in scope.
191#[allow(dead_code)]
192fn _assert_graphql_type_in_scope<T: GraphqlType>(_: &T) {}