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