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