surreal_simple_querybuilder/node_builder.rs
1use std::fmt::Display;
2
3pub trait ToNodeBuilder<T: Display = Self>: Display {
4 fn quoted(&self) -> String {
5 format!("\"{self}\"")
6 }
7
8 /// Draws the start of a relation `->node`
9 ///
10 /// # Example
11 /// ```
12 /// use surreal_simple_querybuilder::prelude::*;
13 ///
14 /// let s = "user".with("project");
15 ///
16 /// assert_eq!("user->project", s);
17 /// ```
18 fn with(&self, relation_or_node: &str) -> String {
19 // write the arrow only if the first character is not a special character.
20 // there are cases where the `node` string that was passed starts with
21 // an arrow or a dot, in which case we do not want to push a new arrow
22 // ourselves.
23 if !relation_or_node.starts_with("->") && !relation_or_node.starts_with(".") {
24 format!("{self}->{relation_or_node}")
25 } else {
26 format!("{self}{relation_or_node}")
27 }
28 }
29
30 /// Draws the end of a relation `<-node`
31 ///
32 /// # Example
33 /// ```
34 /// use surreal_simple_querybuilder::prelude::*;
35 ///
36 /// let s = "user".from("project");
37 ///
38 /// assert_eq!("user<-project", s);
39 /// ```
40 fn from(&self, node: &str) -> String {
41 format!("{self}<-{node}")
42 }
43
44 /// Take the current string and add in front of it the given label name as to
45 /// make a string of the following format `LabelName:CurrentString`
46 ///
47 /// # Example
48 /// ```
49 /// use surreal_simple_querybuilder::prelude::*;
50 ///
51 /// let label = "John".as_named_label("Account");
52 ///
53 /// assert_eq!(label, "Account:John");
54 /// ```
55 fn as_named_label(&self, label_name: &str) -> String {
56 format!("{label_name}:{self}")
57 }
58
59 fn as_param(&self) -> String {
60 self
61 .to_string()
62 .replace(".", "_")
63 .replace("->", "_")
64 .replace("<-", "_")
65 }
66
67 /// # Example
68 /// ```
69 /// use surreal_simple_querybuilder::prelude::*;
70 ///
71 /// let s = "user".equals("John");
72 ///
73 /// // Note that it doesn't add quotes around strings
74 /// assert_eq!("user = John", s);
75 /// ```
76 fn equals(&self, value: &str) -> String {
77 format!("{self} = {value}")
78 }
79
80 /// # Example
81 /// ```
82 /// use surreal_simple_querybuilder::prelude::*;
83 ///
84 /// let s = "age".compares(">=", "45");
85 ///
86 /// assert_eq!("age >= 45", s);
87 /// ```
88 fn compares(&self, operator: &str, value: &str) -> String {
89 format!("{self} {operator} {value}")
90 }
91
92 /// Take the current string and add the given operator plus ` $current_string` after it
93 ///
94 /// # Example
95 /// ```
96 /// use surreal_simple_querybuilder::prelude::*;
97 ///
98 /// let s = "age".compares_parameterized(">=");
99 ///
100 /// assert_eq!("age >= $age", s);
101 /// ```
102 fn compares_parameterized(&self, operator: &str) -> String {
103 format!("{self} {operator} ${}", self.as_param())
104 }
105
106 /// Take the current string and add `= $current_string` after it
107 ///
108 /// # Example
109 /// ```
110 /// use surreal_simple_querybuilder::prelude::*;
111 ///
112 /// let s = "account".equals_parameterized();
113 ///
114 /// assert_eq!("account = $account", s);
115 /// ```
116 fn equals_parameterized(&self) -> String {
117 format!("{self} = ${}", self.as_param())
118 }
119
120 /// Take the current string and add `+= $current_string` after it
121 ///
122 /// # Example
123 /// ```
124 /// use surreal_simple_querybuilder::prelude::*;
125 ///
126 /// let s = "account".plus_equal_parameterized();
127 ///
128 /// assert_eq!("account += $account", s);
129 /// ```
130 fn plus_equal_parameterized(&self) -> String {
131 format!("{self} += ${}", self.as_param())
132 }
133
134 /// Take the current string and add `> $current_string` after it
135 ///
136 /// # Example
137 /// ```
138 /// use surreal_simple_querybuilder::prelude::*;
139 ///
140 /// let s = "age".greater_parameterized();
141 ///
142 /// assert_eq!("age > $age", s);
143 /// ```
144 fn greater_parameterized(&self) -> String {
145 format!("{self} > ${}", self.as_param())
146 }
147
148 /// Take the current string and add `< $current_string` after it
149 ///
150 /// # Example
151 /// ```
152 /// use surreal_simple_querybuilder::prelude::*;
153 ///
154 /// let s = "age".lower_parameterized();
155 ///
156 /// assert_eq!("age < $age", s);
157 /// ```
158 fn lower_parameterized(&self) -> String {
159 format!("{self} < ${}", self.as_param())
160 }
161
162 /// Take the current string and add `> value` after it
163 ///
164 /// # Example
165 /// ```
166 /// use surreal_simple_querybuilder::prelude::*;
167 ///
168 /// let s = "account".greater_than("5");
169 ///
170 /// assert_eq!("account > 5", s);
171 /// ```
172 fn greater_than(&self, value: &str) -> String {
173 format!("{self} > {value}")
174 }
175
176 /// Take the current string and add `+= value` after it
177 ///
178 /// # Example
179 /// ```
180 /// use surreal_simple_querybuilder::prelude::*;
181 ///
182 /// let s = "friends".plus_equal("account:john");
183 ///
184 /// assert_eq!("friends += account:john", s);
185 /// ```
186 fn plus_equal(&self, value: &str) -> String {
187 format!("{self} += {value}")
188 }
189
190 /// # Example
191 /// ```
192 /// use surreal_simple_querybuilder::prelude::*;
193 ///
194 /// let s = "account".contains_one("'c'");
195 ///
196 /// assert_eq!("account CONTAINS 'c'", s);
197 /// ```
198 fn contains_one(&self, value: &str) -> String {
199 format!("{self} CONTAINS {value}")
200 }
201
202 /// # Example
203 /// ```
204 /// use surreal_simple_querybuilder::prelude::*;
205 ///
206 /// let s = "account".contains_not("'z'");
207 ///
208 /// assert_eq!("account CONTAINSNOT 'z'", s);
209 /// ```
210 fn contains_not(&self, value: &str) -> String {
211 format!("{self} CONTAINSNOT {value}")
212 }
213
214 /// # Example
215 /// ```
216 /// use surreal_simple_querybuilder::prelude::*;
217 ///
218 /// let s = "account".contains_all("['a', 'c', 'u']");
219 ///
220 /// assert_eq!("account CONTAINSALL ['a', 'c', 'u']", s);
221 /// ```
222 fn contains_all(&self, values: &str) -> String {
223 format!("{self} CONTAINSALL {values}")
224 }
225
226 /// # Example
227 /// ```
228 /// use surreal_simple_querybuilder::prelude::*;
229 ///
230 /// let s = "account".contains_any("['a', 'c', 'u']");
231 ///
232 /// assert_eq!("account CONTAINSANY ['a', 'c', 'u']", s);
233 /// ```
234 fn contains_any(&self, values: &str) -> String {
235 format!("{self} CONTAINSANY {values}")
236 }
237
238 /// # Example
239 /// ```
240 /// use surreal_simple_querybuilder::prelude::*;
241 ///
242 /// let s = "account".contains_none("['z', 'd', 'f']");
243 ///
244 /// assert_eq!("account CONTAINSNONE ['z', 'd', 'f']", s);
245 /// ```
246 fn contains_none(&self, values: &str) -> String {
247 format!("{self} CONTAINSNONE {values}")
248 }
249
250 /// Take the current string and add `as alias` after it
251 ///
252 /// # Example
253 /// ```
254 /// use surreal_simple_querybuilder::prelude::*;
255 ///
256 /// let s = "account->manage->project".as_alias("account_projects");
257 ///
258 /// assert_eq!("account->manage->project AS account_projects", s);
259 /// ```
260 fn as_alias(&self, alias: &str) -> String {
261 format!("{self} AS {alias}")
262 }
263
264 /// Take the current string, extract the last segment if it is a nested property,
265 /// then add parenthesis around it and add the supplied condition in them.
266 ///
267 /// # Example
268 /// ```
269 /// use surreal_simple_querybuilder::prelude::*;
270 ///
271 /// let path = "account->manage->project";
272 /// let s = path.filter("name = 'a_cool_project'");
273 ///
274 /// assert_eq!("account->manage->(project WHERE name = 'a_cool_project')", s);
275 /// ```
276 ///
277 fn filter(&self, condition: &str) -> String {
278 // This is a default implementation, but since we need the original string
279 // to iterate over the chars the function does two string allocations.
280 let original = self.to_string();
281 let original_size = original.len();
282
283 // this yields the size of the last segment, until a non alphanumeric character
284 // is found.
285 let last_segment_size = original
286 .chars()
287 .rev()
288 .take_while(|c| c.is_alphanumeric())
289 .count();
290
291 let left = &original[..original_size - last_segment_size];
292 let right = &original[original_size - last_segment_size..];
293
294 format!("{left}({right} WHERE {condition})")
295 }
296
297 /// write a comma at the end of the string and append `right` after it.
298 ///
299 /// # Example
300 /// ```
301 /// use surreal_simple_querybuilder::prelude::*;
302 ///
303 /// let select = "*".comma("<-manage<-User as authors");
304 /// let query = format!("select {select} from Files");
305 ///
306 /// assert_eq!("select *, <-manage<-User as authors from Files", query);
307 /// ```
308 fn comma(&self, right: &str) -> String {
309 format!("{self}, {right}")
310 }
311
312 /// write a `count()` around the current string so that it sits between the
313 /// parenthesis.
314 ///
315 /// # Example
316 /// ```
317 /// use surreal_simple_querybuilder::prelude::*;
318 ///
319 /// let count = "id".count();
320 /// let query = format!("select {count} from Files");
321 ///
322 /// assert_eq!("select count(id) from Files", query);
323 /// ```
324 fn count(&self) -> String {
325 format!("count({self})")
326 }
327
328 /// Add the supplied `id` right after the current string in order to get the a
329 /// new string in the following format `current:id`
330 /// # Example
331 /// ```
332 /// use surreal_simple_querybuilder::prelude::*;
333 ///
334 /// let query = "Account".with_id("John");
335 ///
336 /// assert_eq!(query, "Account:John");
337 /// ```
338 fn with_id(&self, id: &str) -> String {
339 format!("{self}:{id}")
340 }
341
342 /// Add the supplied composite `id` right after the current string in order to
343 /// get the a new string in the following format `current:⟨id⟩`. The `⟨` and `⟩`
344 /// are automatically added around the id, if you wish to set a regular id then
345 /// refer to [with_id()](ToNodeBuilder::with_id)
346 ///
347 /// # Example
348 /// ```
349 /// use surreal_simple_querybuilder::prelude::*;
350 ///
351 /// let query = "Account".with_composite_id("John/Doe");
352 ///
353 /// assert_eq!(query, "Account:⟨John/Doe⟩");
354 /// ```
355 fn with_composite_id(&self, id: &str) -> String {
356 format!("{self}:⟨{id}⟩")
357 }
358}
359
360impl<'a> ToNodeBuilder for &'a str {
361 fn filter(&self, condition: &str) -> String {
362 // unlike the default implementation of this trait function, the &str impl
363 // does only one allocation.
364 let original_size = self.len();
365
366 // this yields the size of the last segment, until a non alphanumeric character
367 // is found.
368 let last_segment_size = self
369 .chars()
370 .rev()
371 .take_while(|c| c.is_alphanumeric())
372 .count();
373
374 let left = &self[..original_size - last_segment_size];
375 let right = &self[original_size - last_segment_size..];
376
377 format!("{left}({right} WHERE {condition})")
378 }
379}
380
381pub trait NodeBuilder<T: Display = Self>: Display {
382 /// Draws the start of a relation `->node`
383 ///
384 /// # Example
385 /// ```
386 /// use surreal_simple_querybuilder::prelude::*;
387 ///
388 /// let s = "user".with("project");
389 ///
390 /// assert_eq!("user->project", s);
391 /// ```
392 fn with(&mut self, relation_or_node: &str) -> &mut String;
393
394 /// Allows you to pass a lambda that should mutate the current string when the
395 /// passed `condition` is `true`. If `condition` is `false` then the `action`
396 /// lambda is ignored and the string stays intact.
397 ///
398 /// # Example
399 /// ```
400 /// use surreal_simple_querybuilder::prelude::*;
401 ///
402 /// // demonstrate how the given closure is ignored if the condition is `false`
403 /// let mut label = "John".as_named_label("User");
404 /// let intact = &mut label
405 /// .if_then(false, |s| s.with("LOVES").with("User"))
406 /// .with("FRIEND")
407 /// .with("User");
408 ///
409 /// assert_eq!("User:John->FRIEND->User", *intact);
410 ///
411 /// // demonstrate how the given closure is executed if the condition is `true`
412 /// let mut label = "John".as_named_label("User");
413 /// let modified = &mut label
414 /// .if_then(true, |s| s.with("LOVES").with("User"))
415 /// .with("FRIEND")
416 /// .with("User");
417 ///
418 /// assert_eq!("User:John->LOVES->User->FRIEND->User", *modified);
419 /// ```
420 fn if_then(&mut self, condition: bool, action: fn(&mut Self) -> &mut Self) -> &mut String;
421
422 /// Take the current string add add `> value` after it
423 ///
424 /// # Example
425 /// ```
426 /// use surreal_simple_querybuilder::prelude::*;
427 ///
428 /// let s = "account".greater_than("5");
429 ///
430 /// assert_eq!("account > 5", s);
431 /// ```
432 fn greater_than(&mut self, value: &str) -> &mut String;
433
434 /// Take the current string and add `+= value` after it
435 ///
436 /// # Example
437 /// ```
438 /// use surreal_simple_querybuilder::prelude::*;
439 ///
440 /// let s = "friends".plus_equal("account:john");
441 ///
442 /// assert_eq!("friends += account:john", s);
443 /// ```
444 fn plus_equal(&mut self, value: &str) -> &mut String;
445}
446
447impl NodeBuilder for String {
448 fn with(&mut self, node: &str) -> &mut String {
449 // push the arrow only if the first character is not a special character.
450 // there are cases where the `node` string that was passed starts with
451 // an arrow or a dot, in which case we do not want to push a new arrow
452 // ourselves.
453 if !node.starts_with("->") && !node.starts_with(".") {
454 self.push_str("->");
455 }
456
457 self.push_str(node);
458
459 self
460 }
461
462 fn if_then(&mut self, condition: bool, action: fn(&mut Self) -> &mut Self) -> &mut String {
463 match condition {
464 true => action(self),
465 false => self,
466 }
467 }
468
469 fn greater_than(&mut self, value: &str) -> &mut String {
470 self.push_str(" > ");
471 self.push_str(value);
472
473 self
474 }
475
476 fn plus_equal(&mut self, value: &str) -> &mut String {
477 self.push_str(" += ");
478 self.push_str(value);
479
480 self
481 }
482}
483
484impl ToNodeBuilder for String {}