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 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 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}