issuecraft_ql/
ast.rs

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