fraiseql_wire/operators/
field.rs1use std::fmt;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum Field {
30 JsonbField(String),
36
37 DirectColumn(String),
43
44 JsonbPath(Vec<String>),
53}
54
55impl Field {
56 pub fn validate(&self) -> Result<(), String> {
66 let name = match self {
67 Field::JsonbField(n) => n,
68 Field::DirectColumn(n) => n,
69 Field::JsonbPath(path) => {
70 for segment in path {
71 if !is_valid_field_name(segment) {
72 return Err(format!("Invalid field name in path: {}", segment));
73 }
74 }
75 return Ok(());
76 }
77 };
78
79 if !is_valid_field_name(name) {
80 return Err(format!("Invalid field name: {}", name));
81 }
82
83 Ok(())
84 }
85
86 pub fn to_sql(&self) -> String {
88 match self {
89 Field::JsonbField(name) => format!("(data->'{}')", name),
90 Field::DirectColumn(name) => name.clone(),
91 Field::JsonbPath(path) => {
92 if path.is_empty() {
93 return "data".to_string();
94 }
95
96 let mut sql = String::from("(data");
97 for (i, segment) in path.iter().enumerate() {
98 if i == path.len() - 1 {
99 sql.push_str(&format!("->>'{}\'", segment));
101 } else {
102 sql.push_str(&format!("->'{}\'", segment));
104 }
105 }
106 sql.push(')');
107 sql
108 }
109 }
110 }
111}
112
113impl fmt::Display for Field {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 match self {
116 Field::JsonbField(name) => write!(f, "data->'{}'", name),
117 Field::DirectColumn(name) => write!(f, "{}", name),
118 Field::JsonbPath(path) => {
119 write!(f, "data")?;
120 for (i, segment) in path.iter().enumerate() {
121 if i == path.len() - 1 {
122 write!(f, "->>{}", segment)?;
123 } else {
124 write!(f, "->{}", segment)?;
125 }
126 }
127 Ok(())
128 }
129 }
130 }
131}
132
133#[derive(Debug, Clone)]
147#[non_exhaustive]
148pub enum Value {
149 String(String),
151
152 Number(f64),
154
155 Bool(bool),
157
158 Null,
160
161 Array(Vec<Value>),
163
164 FloatArray(Vec<f32>),
166
167 RawSql(String),
172}
173
174impl Value {
175 pub const fn is_null(&self) -> bool {
177 matches!(self, Value::Null)
178 }
179
180 pub fn to_sql_literal(&self) -> String {
185 match self {
186 Value::String(s) => format!("'{}'", s.replace('\'', "''")),
187 Value::Number(n) => n.to_string(),
188 Value::Bool(b) => b.to_string(),
189 Value::Null => "NULL".to_string(),
190 Value::Array(arr) => {
191 let items: Vec<String> = arr.iter().map(|v| v.to_sql_literal()).collect();
192 format!("ARRAY[{}]", items.join(", "))
193 }
194 Value::FloatArray(arr) => {
195 let items: Vec<String> = arr.iter().map(|f| f.to_string()).collect();
196 format!("[{}]", items.join(", "))
197 }
198 Value::RawSql(sql) => sql.clone(),
199 }
200 }
201}
202
203impl fmt::Display for Value {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 write!(f, "{}", self.to_sql_literal())
206 }
207}
208
209fn is_valid_field_name(name: &str) -> bool {
211 if name.is_empty() {
212 return false;
213 }
214
215 let first = name
217 .chars()
218 .next()
219 .expect("empty name already returned false above");
220 if !first.is_alphabetic() && first != '_' {
221 return false;
222 }
223
224 name.chars().all(|c| c.is_alphanumeric() || c == '_')
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_valid_field_names() {
234 assert!(is_valid_field_name("name"));
235 assert!(is_valid_field_name("_private"));
236 assert!(is_valid_field_name("field_123"));
237 assert!(is_valid_field_name("a"));
238 }
239
240 #[test]
241 fn test_invalid_field_names() {
242 assert!(!is_valid_field_name(""));
243 assert!(!is_valid_field_name("123field")); assert!(!is_valid_field_name("field-name")); assert!(!is_valid_field_name("field.name")); assert!(!is_valid_field_name("field'name")); }
248
249 #[test]
250 fn test_field_validation() {
251 Field::JsonbField("name".to_string())
252 .validate()
253 .unwrap_or_else(|e| panic!("expected Ok for valid field 'name': {e}"));
254
255 let result = Field::JsonbField("name-invalid".to_string()).validate();
256 assert!(
257 result.is_err(),
258 "expected Err for field 'name-invalid', got: {result:?}"
259 );
260
261 Field::JsonbPath(vec!["user".to_string(), "name".to_string()])
262 .validate()
263 .unwrap_or_else(|e| panic!("expected Ok for valid JsonbPath [user, name]: {e}"));
264 }
265
266 #[test]
267 fn test_field_to_sql_jsonb() {
268 let field = Field::JsonbField("name".to_string());
269 assert_eq!(field.to_sql(), "(data->'name')");
270 }
271
272 #[test]
273 fn test_field_to_sql_direct() {
274 let field = Field::DirectColumn("created_at".to_string());
275 assert_eq!(field.to_sql(), "created_at");
276 }
277
278 #[test]
279 fn test_field_to_sql_path() {
280 let field = Field::JsonbPath(vec!["user".to_string(), "name".to_string()]);
281 assert_eq!(field.to_sql(), "(data->'user'->>'name')");
282 }
283
284 #[test]
285 fn test_value_to_sql_literal() {
286 assert_eq!(Value::String("test".to_string()).to_sql_literal(), "'test'");
287 assert_eq!(Value::Number(42.0).to_sql_literal(), "42");
288 assert_eq!(Value::Bool(true).to_sql_literal(), "true");
289 assert_eq!(Value::Null.to_sql_literal(), "NULL");
290 }
291
292 #[test]
293 fn test_value_string_escaping() {
294 let val = Value::String("O'Brien".to_string());
295 assert_eq!(val.to_sql_literal(), "'O''Brien'");
296 }
297}