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