issuecraft_ql/
ast.rs

1use std::fmt;
2
3use facet_value::Value as FacetValue;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum Statement {
7    Create(CreateStatement),
8    Select(SelectStatement),
9    Update(UpdateStatement),
10    Delete(DeleteStatement),
11    Assign(AssignStatement),
12    Close(CloseStatement),
13    Comment(CommentStatement),
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum CreateStatement {
18    User {
19        username: String,
20        email: Option<String>,
21        name: Option<String>,
22    },
23    Project {
24        project_id: String,
25        name: Option<String>,
26        description: Option<String>,
27        owner: Option<String>,
28    },
29    Issue {
30        project: String,
31        title: String,
32        description: Option<String>,
33        priority: Option<Priority>,
34        assignee: Option<String>,
35        labels: Vec<String>,
36    },
37    Comment {
38        issue_id: IssueId,
39        content: String,
40        author: Option<String>,
41    },
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub struct SelectStatement {
46    pub columns: Columns,
47    pub from: EntityType,
48    pub filter: Option<FilterExpression>,
49    pub order_by: Option<OrderBy>,
50    pub limit: Option<u32>,
51    pub offset: Option<u32>,
52}
53
54#[derive(Debug, Clone, PartialEq)]
55pub enum Columns {
56    All,
57    Named(Vec<String>),
58}
59
60impl Columns {
61    pub fn len(&self) -> usize {
62        match self {
63            Columns::All => usize::MAX,
64            Columns::Named(cols) => cols.len(),
65        }
66    }
67}
68
69#[derive(Debug, Clone, PartialEq)]
70pub enum EntityType {
71    Users,
72    Projects,
73    Issues,
74    Comments,
75}
76
77impl fmt::Display for EntityType {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            EntityType::Users => write!(f, "users"),
81            EntityType::Projects => write!(f, "projects"),
82            EntityType::Issues => write!(f, "issues"),
83            EntityType::Comments => write!(f, "comments"),
84        }
85    }
86}
87
88#[derive(Debug, Clone, PartialEq)]
89pub enum FilterExpression {
90    Comparison {
91        field: String,
92        op: ComparisonOp,
93        value: Value,
94    },
95    And(Box<FilterExpression>, Box<FilterExpression>),
96    Or(Box<FilterExpression>, Box<FilterExpression>),
97    Not(Box<FilterExpression>),
98    In {
99        field: String,
100        values: Vec<Value>,
101    },
102    IsNull(String),
103    IsNotNull(String),
104}
105
106impl FilterExpression {
107    pub fn matches(&self, value: &FacetValue) -> bool {
108        match self {
109            FilterExpression::Comparison {
110                field,
111                op,
112                value: filter_value,
113            } => {
114                let obj = match value.as_object() {
115                    Some(obj) => obj,
116                    None => return false,
117                };
118
119                let field_value = match obj.get(field) {
120                    Some(v) => v,
121                    None => return false,
122                };
123
124                Self::compare_values(field_value, op, filter_value)
125            }
126            FilterExpression::And(left, right) => left.matches(value) && right.matches(value),
127            FilterExpression::Or(left, right) => left.matches(value) || right.matches(value),
128            FilterExpression::Not(expr) => !expr.matches(value),
129            FilterExpression::In { field, values } => {
130                let obj = match value.as_object() {
131                    Some(obj) => obj,
132                    None => return false,
133                };
134
135                let field_value = match obj.get(field) {
136                    Some(v) => v,
137                    None => return false,
138                };
139
140                values.iter().any(|filter_val| {
141                    Self::compare_values(field_value, &ComparisonOp::Equal, filter_val)
142                })
143            }
144            FilterExpression::IsNull(field) => {
145                let obj = match value.as_object() {
146                    Some(obj) => obj,
147                    None => return false,
148                };
149
150                match obj.get(field) {
151                    None => true,
152                    Some(v) => v.is_null(),
153                }
154            }
155            FilterExpression::IsNotNull(field) => {
156                let obj = match value.as_object() {
157                    Some(obj) => obj,
158                    None => return false,
159                };
160
161                match obj.get(field) {
162                    None => false,
163                    Some(v) => !v.is_null(),
164                }
165            }
166        }
167    }
168
169    fn compare_values(field_value: &FacetValue, op: &ComparisonOp, filter_value: &Value) -> bool {
170        match op {
171            ComparisonOp::Equal => {
172                if let Some(converted) = Self::convert_iql_value_to_facet(filter_value) {
173                    field_value == &converted
174                } else {
175                    false
176                }
177            }
178            ComparisonOp::NotEqual => {
179                if let Some(converted) = Self::convert_iql_value_to_facet(filter_value) {
180                    field_value != &converted
181                } else {
182                    true
183                }
184            }
185            ComparisonOp::GreaterThan => {
186                if let Some(converted) = Self::convert_iql_value_to_facet(filter_value) {
187                    field_value.partial_cmp(&converted) == Some(std::cmp::Ordering::Greater)
188                } else {
189                    false
190                }
191            }
192            ComparisonOp::LessThan => {
193                if let Some(converted) = Self::convert_iql_value_to_facet(filter_value) {
194                    field_value.partial_cmp(&converted) == Some(std::cmp::Ordering::Less)
195                } else {
196                    false
197                }
198            }
199            ComparisonOp::GreaterThanOrEqual => {
200                if let Some(converted) = Self::convert_iql_value_to_facet(filter_value) {
201                    matches!(
202                        field_value.partial_cmp(&converted),
203                        Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
204                    )
205                } else {
206                    false
207                }
208            }
209            ComparisonOp::LessThanOrEqual => {
210                if let Some(converted) = Self::convert_iql_value_to_facet(filter_value) {
211                    matches!(
212                        field_value.partial_cmp(&converted),
213                        Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
214                    )
215                } else {
216                    false
217                }
218            }
219            ComparisonOp::Like => {
220                let field_str = field_value.as_string().map(|s| s.as_str()).unwrap_or("");
221                if let Value::String(pattern) = filter_value {
222                    let pattern = pattern.replace("%", ".*");
223                    if let Ok(regex) = regex::Regex::new(&format!("^{}$", pattern)) {
224                        regex.is_match(field_str)
225                    } else {
226                        false
227                    }
228                } else {
229                    false
230                }
231            }
232        }
233    }
234
235    fn convert_iql_value_to_facet(iql_value: &Value) -> Option<FacetValue> {
236        match iql_value {
237            Value::String(s) => Some(facet_value::VString::new(s).into_value()),
238            Value::Number(n) => Some(facet_value::VNumber::from_u64(*n as u64).into_value()),
239            Value::Float(f) => Some(facet_value::VNumber::from_f64(*f as f64)?.into_value()),
240            Value::Boolean(b) => Some(if *b {
241                facet_value::Value::TRUE
242            } else {
243                facet_value::Value::FALSE
244            }),
245            Value::Null => Some(facet_value::Value::NULL),
246            Value::Priority(p) => Some(facet_value::VString::new(&p.to_string()).into_value()),
247            Value::Identifier(id) => Some(facet_value::VString::new(id).into_value()),
248        }
249    }
250}
251
252#[derive(Debug, Clone, PartialEq)]
253pub enum ComparisonOp {
254    Equal,
255    NotEqual,
256    GreaterThan,
257    LessThan,
258    GreaterThanOrEqual,
259    LessThanOrEqual,
260    Like,
261}
262
263#[derive(Debug, Clone, PartialEq)]
264pub struct OrderBy {
265    pub field: String,
266    pub direction: OrderDirection,
267}
268
269#[derive(Debug, Clone, PartialEq)]
270pub enum OrderDirection {
271    Asc,
272    Desc,
273}
274
275#[derive(Debug, Clone, PartialEq)]
276pub struct UpdateStatement {
277    pub entity: UpdateTarget,
278    pub updates: Vec<FieldUpdate>,
279}
280
281#[derive(Debug, Clone, PartialEq)]
282pub enum UpdateTarget {
283    User(String),
284    Project(String),
285    Issue(IssueId),
286    Comment(u64),
287}
288
289#[derive(Debug, Clone, PartialEq)]
290pub struct FieldUpdate {
291    pub field: String,
292    pub value: Value,
293}
294
295#[derive(Debug, Clone, PartialEq)]
296pub struct DeleteStatement {
297    pub entity: DeleteTarget,
298}
299
300#[derive(Debug, Clone, PartialEq)]
301pub enum DeleteTarget {
302    User(String),
303    Project(String),
304    Issue(IssueId),
305    Comment(u64),
306}
307
308#[derive(Debug, Clone, PartialEq)]
309pub struct AssignStatement {
310    pub issue_id: IssueId,
311    pub assignee: String,
312}
313
314#[derive(Debug, Clone, PartialEq)]
315pub struct CloseStatement {
316    pub issue_id: IssueId,
317    pub reason: Option<String>,
318}
319
320#[derive(Debug, Clone, PartialEq)]
321pub struct CommentStatement {
322    pub issue_id: IssueId,
323    pub content: String,
324}
325
326#[derive(Debug, Clone, PartialEq)]
327pub struct IssueId {
328    pub project: String,
329    pub number: u64,
330}
331
332impl fmt::Display for IssueId {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        write!(f, "{}#{}", self.project, self.number)
335    }
336}
337
338#[derive(Debug, Clone, PartialEq)]
339pub enum Priority {
340    Critical,
341    High,
342    Medium,
343    Low,
344}
345
346impl fmt::Display for Priority {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        match self {
349            Priority::Critical => write!(f, "critical"),
350            Priority::High => write!(f, "high"),
351            Priority::Medium => write!(f, "medium"),
352            Priority::Low => write!(f, "low"),
353        }
354    }
355}
356
357#[derive(Debug, Clone, PartialEq)]
358pub enum Value {
359    String(String),
360    Number(i64),
361    Float(f64),
362    Boolean(bool),
363    Null,
364    Priority(Priority),
365    Identifier(String),
366}
367
368impl fmt::Display for Value {
369    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370        match self {
371            Value::String(s) => write!(f, "'{}'", s),
372            Value::Number(n) => write!(f, "{}", n),
373            Value::Float(fl) => write!(f, "{}", fl),
374            Value::Boolean(b) => write!(f, "{}", b),
375            Value::Null => write!(f, "NULL"),
376            Value::Priority(p) => write!(f, "{}", p),
377            Value::Identifier(id) => write!(f, "{}", id),
378        }
379    }
380}