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}