issuecraft_ql/
ast.rs

1use std::fmt;
2
3use facet::{Facet, Type};
4use facet_value::Value as FacetValue;
5
6use crate::IqlError;
7
8#[derive(Debug, Clone, PartialEq)]
9pub enum IqlQuery {
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        kind: IssueKind,
102        description: Option<String>,
103        priority: Option<Priority>,
104        assignee: Option<UserId>,
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 count(&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<'a, S: Facet<'a>>(&self, value: &mut FacetValue) -> Result<(), IqlError> {
362        let o = value.as_object_mut().unwrap();
363        if let Type::User(facet::UserType::Struct(s)) = S::SHAPE.ty {
364            if !s.fields.iter().any(|f| f.name == self.field) {
365                return Err(IqlError::FieldNotFound(self.field.clone()));
366            }
367        } else {
368            panic!("Not a struct type");
369        }
370        o.insert(&self.field, self.value.to_facet());
371        Ok(())
372    }
373}
374
375#[derive(Debug, Clone, PartialEq)]
376pub struct DeleteStatement {
377    pub entity: DeleteTarget,
378}
379
380#[derive(Debug, Clone, PartialEq)]
381pub enum DeleteTarget {
382    User(String),
383    Project(String),
384    Issue(IssueId),
385    Comment(u64),
386}
387
388#[derive(Debug, Clone, Facet, PartialEq)]
389#[repr(C)]
390pub enum IssueKind {
391    Epic,
392    Improvement,
393    Bug,
394    Task,
395}
396
397#[derive(Debug, Clone, PartialEq)]
398pub struct AssignStatement {
399    pub issue_id: IssueId,
400    pub assignee: String,
401}
402
403#[derive(Debug, Clone, PartialEq, Facet, Default)]
404#[repr(C)]
405pub enum CloseReason {
406    #[default]
407    Done,
408    Duplicate,
409    WontFix,
410}
411
412impl fmt::Display for CloseReason {
413    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
414        match self {
415            CloseReason::Done => write!(f, "DONE"),
416            CloseReason::Duplicate => write!(f, "DUPLICATE"),
417            CloseReason::WontFix => write!(f, "WONTFIX"),
418        }
419    }
420}
421
422#[derive(Debug, Clone, PartialEq)]
423pub struct CloseStatement {
424    pub issue_id: IssueId,
425    pub reason: Option<CloseReason>,
426}
427
428#[derive(Debug, Clone, PartialEq)]
429pub struct ReopenStatement {
430    pub issue_id: IssueId,
431}
432
433#[derive(Debug, Clone, PartialEq)]
434pub struct CommentStatement {
435    pub issue_id: IssueId,
436    pub content: String,
437}
438
439#[derive(Debug, Clone, PartialEq)]
440pub enum Priority {
441    Critical,
442    High,
443    Medium,
444    Low,
445}
446
447impl fmt::Display for Priority {
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        match self {
450            Priority::Critical => write!(f, "CRITICAL"),
451            Priority::High => write!(f, "HIGH"),
452            Priority::Medium => write!(f, "MEDIUM"),
453            Priority::Low => write!(f, "LOW"),
454        }
455    }
456}
457
458#[derive(Debug, Clone, PartialEq)]
459pub enum IqlValue {
460    String(String),
461    Number(i64),
462    Float(f64),
463    Boolean(bool),
464    Null,
465    Priority(Priority),
466    Identifier(String),
467}
468
469impl IqlValue {
470    fn to_facet(&self) -> FacetValue {
471        match self {
472            IqlValue::String(s) => facet_value::VString::new(s).into_value(),
473            IqlValue::Number(n) => facet_value::VNumber::from_u64(*n as u64).into_value(),
474            IqlValue::Float(f) => facet_value::VNumber::from_f64(*f)
475                .expect("Invalid float value")
476                .into_value(),
477            IqlValue::Boolean(b) => {
478                if *b {
479                    facet_value::Value::TRUE
480                } else {
481                    facet_value::Value::FALSE
482                }
483            }
484            IqlValue::Null => facet_value::Value::NULL,
485            IqlValue::Priority(p) => facet_value::VString::new(&p.to_string()).into_value(),
486            IqlValue::Identifier(id) => facet_value::VString::new(id).into_value(),
487        }
488    }
489}
490
491impl fmt::Display for IqlValue {
492    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493        match self {
494            IqlValue::String(s) => write!(f, "'{}'", s),
495            IqlValue::Number(n) => write!(f, "{}", n),
496            IqlValue::Float(fl) => write!(f, "{}", fl),
497            IqlValue::Boolean(b) => write!(f, "{}", b),
498            IqlValue::Null => write!(f, "NULL"),
499            IqlValue::Priority(p) => write!(f, "{}", p),
500            IqlValue::Identifier(id) => write!(f, "{}", id),
501        }
502    }
503}