issuecraft_ql/
ast.rs

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