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