Skip to main content

geode_client/
quote.rs

1//! GQL identifier and string-literal quoting helpers (port of Go quote.go).
2
3use crate::error::{Error, Result};
4
5/// Validate a GQL identifier. Returns the identifier unchanged when valid,
6/// otherwise [`Error::InvalidIdent`].
7///
8/// Rules (matching the Go reference): non-empty; each character must be an
9/// ASCII letter, digit, or `_` (allowed anywhere); `-` is allowed except as
10/// the first character.
11pub fn quote_ident(name: &str) -> Result<String> {
12    if name.is_empty() {
13        return Err(Error::invalid_ident("identifier is empty"));
14    }
15    for (i, ch) in name.chars().enumerate() {
16        let ok =
17            ch.is_ascii_alphabetic() || ch == '_' || ch.is_ascii_digit() || (ch == '-' && i != 0);
18        if !ok {
19            return Err(Error::invalid_ident(format!(
20                "identifier contains forbidden character {:?}",
21                ch
22            )));
23        }
24    }
25    Ok(name.to_string())
26}
27
28/// Quote a GQL string literal: rejects ASCII control characters, escapes
29/// backslash then single-quote, and wraps the result in single quotes.
30///
31/// # Errors
32/// Returns [`Error::InvalidString`] if `s` contains any character `< 0x20`
33/// or `0x7f` (DEL).
34pub fn quote_string(s: &str) -> Result<String> {
35    for ch in s.chars() {
36        let c = ch as u32;
37        if c < 0x20 || c == 0x7f {
38            return Err(Error::invalid_string(
39                "string literal contains a control character",
40            ));
41        }
42    }
43    // Order matters: backslash first, then single-quote.
44    let escaped = s.replace('\\', "\\\\").replace('\'', "''");
45    Ok(format!("'{}'", escaped))
46}
47
48/// Sanitize a graph name by keeping only `[A-Za-z0-9_-]` and dropping every
49/// other character (including all non-ASCII). Never fails.
50pub fn sanitize_graph_name(name: &str) -> String {
51    name.chars()
52        .filter(|ch| ch.is_ascii_alphanumeric() || *ch == '_' || *ch == '-')
53        .collect()
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::error::Error;
60
61    #[test]
62    fn test_quote_ident_valid() {
63        for name in [
64            "abc",
65            "ABC",
66            "_foo",
67            "foo_bar",
68            "foo-bar",
69            "foo9",
70            "9starts",
71            "005_add_index",
72        ] {
73            assert_eq!(quote_ident(name).unwrap(), name);
74        }
75    }
76
77    #[test]
78    fn test_quote_ident_invalid() {
79        for name in ["", "-starts", "has space", "has;semi", "has\0null"] {
80            assert!(
81                matches!(quote_ident(name), Err(Error::InvalidIdent(_))),
82                "name={:?}",
83                name
84            );
85        }
86    }
87
88    #[test]
89    fn test_quote_string_basic() {
90        assert_eq!(quote_string("hello").unwrap(), "'hello'");
91        assert_eq!(quote_string("it's").unwrap(), "'it''s'");
92    }
93
94    #[test]
95    fn test_quote_string_backslash_then_quote() {
96        // backslash doubled FIRST, then quote doubled.
97        assert_eq!(quote_string("back\\slash").unwrap(), "'back\\\\slash'");
98        assert_eq!(quote_string("back\\'quote").unwrap(), "'back\\\\''quote'");
99    }
100
101    #[test]
102    fn test_quote_string_rejects_control_chars() {
103        for s in ["a\nb", "a\rb", "a\tb", "a\0b", "a\x1fb", "a\x7fb"] {
104            assert!(
105                matches!(quote_string(s), Err(Error::InvalidString(_))),
106                "s={:?}",
107                s
108            );
109        }
110    }
111
112    #[test]
113    fn test_sanitize_graph_name() {
114        assert_eq!(sanitize_graph_name("myGraph"), "myGraph");
115        assert_eq!(sanitize_graph_name("my-graph_01"), "my-graph_01");
116        assert_eq!(sanitize_graph_name("bad name!"), "badname");
117        assert_eq!(sanitize_graph_name(""), "");
118        assert_eq!(sanitize_graph_name("有害"), "");
119        assert_eq!(sanitize_graph_name("   "), "");
120    }
121}