fraiseql_wire/operators/
field.rs1use std::fmt;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Field {
27 JsonbField(String),
33
34 DirectColumn(String),
40
41 JsonbPath(Vec<String>),
50}
51
52impl Field {
53 pub fn validate(&self) -> Result<(), String> {
58 let name = match self {
59 Field::JsonbField(n) => n,
60 Field::DirectColumn(n) => n,
61 Field::JsonbPath(path) => {
62 for segment in path {
63 if !is_valid_field_name(segment) {
64 return Err(format!("Invalid field name in path: {}", segment));
65 }
66 }
67 return Ok(());
68 }
69 };
70
71 if !is_valid_field_name(name) {
72 return Err(format!("Invalid field name: {}", name));
73 }
74
75 Ok(())
76 }
77
78 pub fn to_sql(&self) -> String {
80 match self {
81 Field::JsonbField(name) => format!("(data->'{}')", name),
82 Field::DirectColumn(name) => name.clone(),
83 Field::JsonbPath(path) => {
84 if path.is_empty() {
85 return "data".to_string();
86 }
87
88 let mut sql = String::from("(data");
89 for (i, segment) in path.iter().enumerate() {
90 if i == path.len() - 1 {
91 sql.push_str(&format!("->>'{}\'", segment));
93 } else {
94 sql.push_str(&format!("->'{}\'", segment));
96 }
97 }
98 sql.push(')');
99 sql
100 }
101 }
102 }
103}
104
105impl fmt::Display for Field {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 match self {
108 Field::JsonbField(name) => write!(f, "data->'{}'", name),
109 Field::DirectColumn(name) => write!(f, "{}", name),
110 Field::JsonbPath(path) => {
111 write!(f, "data")?;
112 for (i, segment) in path.iter().enumerate() {
113 if i == path.len() - 1 {
114 write!(f, "->>{}", segment)?;
115 } else {
116 write!(f, "->{}", segment)?;
117 }
118 }
119 Ok(())
120 }
121 }
122 }
123}
124
125#[derive(Debug, Clone)]
137pub enum Value {
138 String(String),
140
141 Number(f64),
143
144 Bool(bool),
146
147 Null,
149
150 Array(Vec<Value>),
152
153 FloatArray(Vec<f32>),
155
156 RawSql(String),
161}
162
163impl Value {
164 pub fn is_null(&self) -> bool {
166 matches!(self, Value::Null)
167 }
168
169 pub fn to_sql_literal(&self) -> String {
174 match self {
175 Value::String(s) => format!("'{}'", s.replace('\'', "''")),
176 Value::Number(n) => n.to_string(),
177 Value::Bool(b) => b.to_string(),
178 Value::Null => "NULL".to_string(),
179 Value::Array(arr) => {
180 let items: Vec<String> = arr.iter().map(|v| v.to_sql_literal()).collect();
181 format!("ARRAY[{}]", items.join(", "))
182 }
183 Value::FloatArray(arr) => {
184 let items: Vec<String> = arr.iter().map(|f| f.to_string()).collect();
185 format!("[{}]", items.join(", "))
186 }
187 Value::RawSql(sql) => sql.clone(),
188 }
189 }
190}
191
192impl fmt::Display for Value {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 write!(f, "{}", self.to_sql_literal())
195 }
196}
197
198fn is_valid_field_name(name: &str) -> bool {
200 if name.is_empty() {
201 return false;
202 }
203
204 let first = name.chars().next().unwrap();
206 if !first.is_alphabetic() && first != '_' {
207 return false;
208 }
209
210 name.chars().all(|c| c.is_alphanumeric() || c == '_')
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_valid_field_names() {
220 assert!(is_valid_field_name("name"));
221 assert!(is_valid_field_name("_private"));
222 assert!(is_valid_field_name("field_123"));
223 assert!(is_valid_field_name("a"));
224 }
225
226 #[test]
227 fn test_invalid_field_names() {
228 assert!(!is_valid_field_name(""));
229 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")); }
234
235 #[test]
236 fn test_field_validation() {
237 assert!(Field::JsonbField("name".to_string()).validate().is_ok());
238 assert!(Field::JsonbField("name-invalid".to_string())
239 .validate()
240 .is_err());
241 assert!(
242 Field::JsonbPath(vec!["user".to_string(), "name".to_string()])
243 .validate()
244 .is_ok()
245 );
246 }
247
248 #[test]
249 fn test_field_to_sql_jsonb() {
250 let field = Field::JsonbField("name".to_string());
251 assert_eq!(field.to_sql(), "(data->'name')");
252 }
253
254 #[test]
255 fn test_field_to_sql_direct() {
256 let field = Field::DirectColumn("created_at".to_string());
257 assert_eq!(field.to_sql(), "created_at");
258 }
259
260 #[test]
261 fn test_field_to_sql_path() {
262 let field = Field::JsonbPath(vec!["user".to_string(), "name".to_string()]);
263 assert_eq!(field.to_sql(), "(data->'user'->>'name')");
264 }
265
266 #[test]
267 fn test_value_to_sql_literal() {
268 assert_eq!(Value::String("test".to_string()).to_sql_literal(), "'test'");
269 assert_eq!(Value::Number(42.0).to_sql_literal(), "42");
270 assert_eq!(Value::Bool(true).to_sql_literal(), "true");
271 assert_eq!(Value::Null.to_sql_literal(), "NULL");
272 }
273
274 #[test]
275 fn test_value_string_escaping() {
276 let val = Value::String("O'Brien".to_string());
277 assert_eq!(val.to_sql_literal(), "'O''Brien'");
278 }
279}