vantage_api_client/graphql/
operation.rs1use serde_json::Value;
21use vantage_expressions::Expressive;
22
23use crate::graphql::condition::{FieldCondition, GraphqlCondition, GraphqlOp};
24use crate::graphql::types::{AnyGraphqlType, GraphqlType};
25
26fn field_name<T>(expr: &(impl Expressive<T> + ?Sized)) -> String {
27 expr.expr().template.clone()
28}
29
30fn to_json(value: impl Into<AnyGraphqlType>) -> Value {
31 value.into().to_json()
32}
33
34fn to_json_array<I, V>(values: I) -> Value
35where
36 I: IntoIterator<Item = V>,
37 V: Into<AnyGraphqlType>,
38{
39 Value::Array(values.into_iter().map(to_json).collect())
40}
41
42pub trait GraphqlOperation<T>: Expressive<T> {
49 fn eq(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
51 where
52 Self: Sized,
53 {
54 GraphqlCondition::Field(FieldCondition::new(
55 field_name(self),
56 GraphqlOp::Eq,
57 to_json(value),
58 ))
59 }
60
61 fn ne(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
63 where
64 Self: Sized,
65 {
66 GraphqlCondition::Field(FieldCondition::new(
67 field_name(self),
68 GraphqlOp::Ne,
69 to_json(value),
70 ))
71 }
72
73 fn gt(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
75 where
76 Self: Sized,
77 {
78 GraphqlCondition::Field(FieldCondition::new(
79 field_name(self),
80 GraphqlOp::Gt,
81 to_json(value),
82 ))
83 }
84
85 fn gte(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
87 where
88 Self: Sized,
89 {
90 GraphqlCondition::Field(FieldCondition::new(
91 field_name(self),
92 GraphqlOp::Gte,
93 to_json(value),
94 ))
95 }
96
97 fn lt(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
99 where
100 Self: Sized,
101 {
102 GraphqlCondition::Field(FieldCondition::new(
103 field_name(self),
104 GraphqlOp::Lt,
105 to_json(value),
106 ))
107 }
108
109 fn lte(&self, value: impl Into<AnyGraphqlType>) -> GraphqlCondition
111 where
112 Self: Sized,
113 {
114 GraphqlCondition::Field(FieldCondition::new(
115 field_name(self),
116 GraphqlOp::Lte,
117 to_json(value),
118 ))
119 }
120
121 fn in_<I, V>(&self, values: I) -> GraphqlCondition
123 where
124 Self: Sized,
125 I: IntoIterator<Item = V>,
126 V: Into<AnyGraphqlType>,
127 {
128 GraphqlCondition::Field(FieldCondition::new(
129 field_name(self),
130 GraphqlOp::In,
131 to_json_array(values),
132 ))
133 }
134
135 fn not_in<I, V>(&self, values: I) -> GraphqlCondition
137 where
138 Self: Sized,
139 I: IntoIterator<Item = V>,
140 V: Into<AnyGraphqlType>,
141 {
142 GraphqlCondition::Field(FieldCondition::new(
143 field_name(self),
144 GraphqlOp::NotIn,
145 to_json_array(values),
146 ))
147 }
148
149 fn like(&self, pattern: impl Into<String>) -> GraphqlCondition
151 where
152 Self: Sized,
153 {
154 GraphqlCondition::Field(FieldCondition::new(
155 field_name(self),
156 GraphqlOp::Like,
157 Value::String(pattern.into()),
158 ))
159 }
160
161 fn ilike(&self, pattern: impl Into<String>) -> GraphqlCondition
163 where
164 Self: Sized,
165 {
166 GraphqlCondition::Field(FieldCondition::new(
167 field_name(self),
168 GraphqlOp::ILike,
169 Value::String(pattern.into()),
170 ))
171 }
172
173 fn is_null(&self) -> GraphqlCondition
175 where
176 Self: Sized,
177 {
178 GraphqlCondition::Field(FieldCondition::new(
179 field_name(self),
180 GraphqlOp::IsNull,
181 Value::Null,
182 ))
183 }
184
185 fn is_not_null(&self) -> GraphqlCondition
187 where
188 Self: Sized,
189 {
190 GraphqlCondition::Field(FieldCondition::new(
191 field_name(self),
192 GraphqlOp::IsNotNull,
193 Value::Null,
194 ))
195 }
196}
197
198impl<T, S: Expressive<T>> GraphqlOperation<T> for S {}
200
201#[allow(dead_code)]
205fn _assert_graphql_type_in_scope<T: GraphqlType>(_: &T) {}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::graphql::condition::FilterDialect;
211 use serde_json::json;
212 use vantage_table::column::core::Column;
213
214 #[tokio::test]
215 async fn column_eq_renders_hasura() {
216 let mission = Column::<String>::new("mission_name");
217 let cond = mission.eq("FalconSat");
218 let r = cond.render(FilterDialect::Hasura).await.unwrap();
219 assert_eq!(r, json!({ "mission_name": { "_eq": "FalconSat" } }));
220 }
221
222 #[tokio::test]
223 async fn column_eq_renders_generic() {
224 let mission = Column::<String>::new("mission_name");
225 let cond = mission.eq("FalconSat");
226 let r = cond.render(FilterDialect::Generic).await.unwrap();
227 assert_eq!(r, json!({ "mission_name": "FalconSat" }));
228 }
229
230 #[tokio::test]
231 async fn column_gt_renders_hasura() {
232 let price = Column::<i64>::new("price");
233 let cond = price.gt(100i64);
234 let r = cond.render(FilterDialect::Hasura).await.unwrap();
235 assert_eq!(r, json!({ "price": { "_gt": 100 } }));
236 }
237
238 #[tokio::test]
239 async fn column_in_renders_hasura_array() {
240 let status = Column::<String>::new("status");
241 let cond = status.in_(vec!["active", "pending"]);
242 let r = cond.render(FilterDialect::Hasura).await.unwrap();
243 assert_eq!(r, json!({ "status": { "_in": ["active", "pending"] } }));
244 }
245
246 #[tokio::test]
247 async fn column_is_null_renders_hasura_bool() {
248 let deleted = Column::<String>::new("deleted_at");
249 let cond = deleted.is_null();
250 let r = cond.render(FilterDialect::Hasura).await.unwrap();
251 assert_eq!(r, json!({ "deleted_at": { "_is_null": true } }));
252 }
253
254 #[tokio::test]
255 async fn column_ilike_renders_hasura() {
256 let name = Column::<String>::new("name");
257 let cond = name.ilike("%falcon%");
258 let r = cond.render(FilterDialect::Hasura).await.unwrap();
259 assert_eq!(r, json!({ "name": { "_ilike": "%falcon%" } }));
260 }
261}