Skip to content
Snippets Groups Projects
Commit 5097d434 authored by Himanshu Kapoor's avatar Himanshu Kapoor
Browse files

Merge branch 'himkp-feat-list-types' into 'main'

feat: add support for typed lists

See merge request !95
parents 43dce9ee 95821682
No related branches found
No related tags found
1 merge request!95feat: add support for typed lists
Pipeline #1615969075 passed
use crate::types::{
Field::{self, *},
Operator, Source, Value,
};
use crate::types::{Field, Field::*, Operator, Source, Value};
use indexmap::set::IndexSet;
impl Field {
pub fn operators(&self, source: &Source) -> IndexSet<Operator> {
source.field_types(self).iter().map(|f| f.operators()).fold(
IndexSet::new(),
|mut acc, set| {
source
.all_field_types(self)
.iter()
.map(|f| f.operators())
.fold(IndexSet::new(), |mut acc, set| {
acc.extend(set);
acc
},
)
})
}
pub fn operators_for_value(&self, value: &Value, source: &Source) -> IndexSet<Operator> {
......@@ -80,7 +78,7 @@ impl Field {
}
pub fn validate_value(&self, value: &Value, source: &Source) -> Result<(), String> {
let types = source.field_types(self);
let types = source.all_field_types(self);
let types_string = types
.iter()
.map(ToString::to_string)
......
......@@ -29,7 +29,9 @@ impl FieldType {
}
_ => false,
},
ListLike(_) => matches!(value, List(_)),
ListLike(_, list_type) => {
matches!(value, List(items) if items.iter().all(|v| list_type.validate(v)))
}
EnumLike(values) | StringEnumLike(values) => match value {
Token(v) | Quoted(v) => values
.iter()
......@@ -39,6 +41,8 @@ impl FieldType {
_ => false,
},
WithOperators(field, _) => field.validate(value),
Multiple(fields) => fields.iter().any(|f| f.validate(value)),
Unsupported => false,
}
}
......@@ -48,8 +52,9 @@ impl FieldType {
| ReferenceLike(..) => vec![Equal, NotEqual],
WithOperators(_, ops) => ops.clone(),
DateLike => vec![Equal, GreaterThan, LessThan],
ListLike(ManyToOne) => vec![In, NotEqual],
ListLike(ManyToMany) => vec![In, Equal, NotEqual],
ListLike(HasOne, _) => vec![In, NotEqual],
ListLike(HasMany, _) => vec![In, Equal, NotEqual],
Multiple(_) | Unsupported => vec![],
}
.into_iter()
.collect()
......
use super::shared::type_field_types;
use super::shared::type_field_type;
use crate::types::{
Field, Field::*, FieldType, FieldType::*, Operator::*, ReferenceType::*, RelationshipType::*,
};
......@@ -27,63 +27,65 @@ pub fn is_valid_field(field: &Field) -> bool {
)
}
pub fn field_types(field: &Field) -> Vec<FieldType> {
pub fn field_type(field: &Field) -> FieldType {
match field {
Type => type_field_types(),
Type => type_field_type(),
Id => vec![
NumberLike.with_operators(&[Equal]),
ListLike(ManyToOne).with_operators(&[In]),
],
Assignee => vec![
StringLike,
ReferenceLike(UserRef),
ListLike(ManyToMany),
NumberLike,
Nullable,
],
Author => vec![
StringLike,
ReferenceLike(UserRef),
ListLike(ManyToOne),
Nullable,
],
Label => vec![
StringLike,
ListLike(ManyToMany),
Nullable,
ReferenceLike(LabelRef),
],
Cadence => vec![StringLike, ListLike(ManyToOne), NumberLike, Nullable],
Milestone => vec![
StringLike,
ListLike(ManyToOne),
Nullable,
EnumLike(vec!["upcoming".into(), "started".into()]),
ReferenceLike(MilestoneRef),
],
Iteration => vec![
StringLike,
ListLike(ManyToOne),
NumberLike,
Nullable,
EnumLike(vec!["current".into()]),
ReferenceLike(IterationRef),
],
Epic => vec![StringLike, NumberLike, Nullable],
Group | Project => vec![StringLike.with_operators(&[Equal])],
Opened | Closed => vec![DateLike, BooleanLike],
Created | Updated | Due => vec![DateLike],
Weight => vec![NumberLike, Nullable],
Confidential => vec![BooleanLike],
Health => vec![
Id => NumberLike.with_ops([Equal]) | ListLike(HasOne, Box::new(NumberLike)).with_ops([In]),
Assignee => {
StringLike
| ReferenceLike(UserRef)
| ListLike(HasMany, Box::new(StringLike | ReferenceLike(UserRef)))
| NumberLike
| Nullable
}
Author => {
StringLike
| ReferenceLike(UserRef)
| ListLike(HasOne, Box::new(StringLike | ReferenceLike(UserRef)))
| Nullable
}
Label => {
StringLike
| ListLike(HasMany, Box::new(StringLike | ReferenceLike(LabelRef)))
| Nullable
| ReferenceLike(LabelRef)
}
Cadence => {
StringLike | ListLike(HasOne, Box::new(NumberLike | StringLike)) | NumberLike | Nullable
}
Milestone => {
StringLike
| ListLike(HasOne, Box::new(StringLike | ReferenceLike(MilestoneRef)))
| Nullable
| EnumLike(vec!["upcoming".into(), "started".into()])
| ReferenceLike(MilestoneRef)
}
Iteration => {
StringLike
| ListLike(
HasOne,
Box::new(NumberLike | StringLike | ReferenceLike(IterationRef)),
)
| NumberLike
| Nullable
| EnumLike(vec!["current".into()])
| ReferenceLike(IterationRef)
}
Epic => StringLike | NumberLike | Nullable,
Group | Project => StringLike.with_ops([Equal]),
Opened | Closed => DateLike | BooleanLike,
Created | Updated | Due => DateLike,
Weight => NumberLike | Nullable,
Confidential => BooleanLike,
Health => {
StringEnumLike(vec![
"on track".into(),
"needs attention".into(),
"at risk".into(),
]),
Nullable,
],
_ => vec![],
]) | Nullable
}
_ => Unsupported,
}
}
use super::shared::type_field_types;
use super::shared::type_field_type;
use crate::types::{
Field, Field::*, FieldType, FieldType::*, Operator::*, ReferenceType::*, RelationshipType::*,
};
......@@ -26,48 +26,44 @@ pub fn is_valid_field(field: &Field) -> bool {
)
}
pub fn field_types(field: &Field) -> Vec<FieldType> {
pub fn field_type(field: &Field) -> FieldType {
match field {
Type => type_field_types(),
Type => type_field_type(),
Id => vec![
NumberLike.with_operators(&[Equal]),
ListLike(ManyToOne).with_operators(&[In]),
],
Assignee => vec![StringLike, ReferenceLike(UserRef), Nullable],
Reviewer | Approver => vec![
StringLike,
ReferenceLike(UserRef),
Nullable,
ListLike(ManyToMany).with_operators(&[Equal, NotEqual]),
],
Author => vec![StringLike, ReferenceLike(UserRef)],
Merger => vec![
StringLike.with_operators(&[Equal]),
ReferenceLike(UserRef).with_operators(&[Equal]),
],
Label => vec![
StringLike,
ListLike(ManyToMany).with_operators(&[Equal, NotEqual]),
Nullable,
ReferenceLike(LabelRef),
],
Milestone => vec![
StringLike,
Nullable,
EnumLike(vec!["upcoming".into(), "started".into()]),
ReferenceLike(MilestoneRef),
],
State => vec![EnumLike(vec![
Id => NumberLike.with_ops([Equal]) | ListLike(HasOne, Box::new(NumberLike)).with_ops([In]),
Assignee => StringLike | ReferenceLike(UserRef) | Nullable,
Reviewer | Approver => {
StringLike
| ReferenceLike(UserRef)
| Nullable
| ListLike(HasMany, Box::new(StringLike | ReferenceLike(UserRef)))
.with_ops([Equal, NotEqual])
}
Author => StringLike | ReferenceLike(UserRef),
Merger => StringLike.with_ops([Equal]) | ReferenceLike(UserRef).with_ops([Equal]),
Label => {
StringLike
| ListLike(HasMany, Box::new(StringLike | ReferenceLike(LabelRef)))
.with_ops([Equal, NotEqual])
| Nullable
| ReferenceLike(LabelRef)
}
Milestone => {
StringLike
| Nullable
| EnumLike(vec!["upcoming".into(), "started".into()])
| ReferenceLike(MilestoneRef)
}
State => EnumLike(vec![
"opened".into(),
"closed".into(),
"merged".into(),
"all".into(),
])
.with_operators(&[Equal])],
Environment | Group | Project => vec![StringLike.with_operators(&[Equal])],
Created | Updated | Merged | Deployed => vec![DateLike],
Draft => vec![BooleanLike],
_ => vec![],
.with_ops([Equal]),
Environment | Group | Project => StringLike.with_ops([Equal]),
Created | Updated | Merged | Deployed => DateLike,
Draft => BooleanLike,
_ => Unsupported,
}
}
use crate::types::{
Field, FieldType,
Source::{self, *},
};
use crate::types::{Field, FieldType, FieldType::*, Source, Source::*};
mod issues;
mod merge_requests;
mod shared;
impl Source {
pub fn field_types(&self, field: &Field) -> Vec<FieldType> {
pub fn all_field_types(&self, field: &Field) -> Vec<FieldType> {
let field_type = self.field_type(field);
if let Multiple(fields) = field_type {
*fields
} else {
vec![self.field_type(field)]
}
}
pub fn field_type(&self, field: &Field) -> FieldType {
match self {
Issues => issues::field_types(field),
MergeRequests => merge_requests::field_types(field),
Issues => issues::field_type(field),
MergeRequests => merge_requests::field_type(field),
}
}
......
use crate::types::{FieldType, FieldType::*, Operator::*};
use crate::types::{FieldType, FieldType::*, Operator::*, RelationshipType::*};
pub fn type_field_types() -> Vec<FieldType> {
vec![EnumLike(vec![
"Issue".into(),
"Incident".into(),
"TestCase".into(),
"Requirement".into(),
"Task".into(),
"Ticket".into(),
"Objective".into(),
"KeyResult".into(),
"Epic".into(),
"MergeRequest".into(),
])
.with_operators(&[Equal])]
const WORK_ITEM_TYPES: [&str; 9] = [
"Issue",
"Incident",
"TestCase",
"Requirement",
"Task",
"Ticket",
"Objective",
"KeyResult",
"Epic",
];
const OTHER_TYPES: [&str; 1] = ["MergeRequest"];
pub fn type_field_type() -> FieldType {
let enum_types: Vec<String> = [WORK_ITEM_TYPES.as_slice(), OTHER_TYPES.as_slice()]
.concat()
.iter()
.map(|&t| t.to_string())
.collect();
let list_enum_types: Vec<String> = WORK_ITEM_TYPES
.as_slice()
.iter()
.map(|&t| t.to_string())
.collect();
EnumLike(enum_types).with_ops([Equal])
| ListLike(HasOne, Box::new(EnumLike(list_enum_types))).with_ops([In])
}
use crate::types::{
Field,
FieldType::{self, *},
RelationshipType::*,
Source,
Value::{self, *},
};
use crate::types::{Field, FieldType, FieldType::*, Source, Value, Value::*};
impl Value {
pub fn field_type(&self, field: &Field, source: &Source) -> Option<FieldType> {
let types = source.field_types(field);
let field_type = source.field_type(field);
let all_field_types = source.all_field_types(field);
match self {
List(l) if types.contains(&ListLike(ManyToMany)) && !l.is_empty() => {
Some(ListLike(ManyToMany))
}
List(l) if types.contains(&ListLike(ManyToOne)) && !l.is_empty() => {
Some(ListLike(ManyToOne))
List(l) if field_type.is_list_like() && !l.is_empty() => {
field_type.find(|f| matches!(f, ListLike(..)))
}
Token(t)
if types.iter().any(
|type_| matches!(type_, EnumLike(s) | StringEnumLike(s) if s.contains(&t.to_lowercase())),
) =>
if field_type.is_enum_like()
&& field_type.enum_values().contains(&t.to_lowercase()) =>
{
Some(EnumLike(vec![t.to_string()]))
field_type.find(|f| matches!(f, EnumLike(_) | StringEnumLike(_)))
}
_ => types.iter().fold(None, |acc, type_| {
_ => all_field_types.iter().fold(None, |acc, type_| {
if type_.validate(self) {
Some(type_.clone())
} else {
acc
}
})
}),
}
}
}
......@@ -205,6 +205,12 @@ fn to_graphql_value(expr: &Expression) -> Value {
(Weight | Epic, Number(n)) => Quoted(n.to_string()),
(State, Token(s)) => Token(s.to_lowercase()),
(Type, Token(s)) => Token(to_type_param(s.as_str())),
(Type, List(types)) => List(
types
.iter()
.map(|i| Token(to_type_param(&i.to_string())))
.collect(),
),
(Health, v) => to_health_status(v),
(Cadence, v) => to_iteration_cadence_id(v),
(_, v) => v.clone(),
......@@ -231,6 +237,7 @@ fn to_graphql_attribute(expr: &Expression, source: &Source) -> String {
(Author, In, _) => pluralize_attribute("authorUsername").to_string(),
(Author, _, _) => "authorUsername".to_string(),
(Id, ..) => "iids".to_string(),
(Type, ..) => "types".to_string(),
(Approver, ..) => "approvedBy".to_string(),
(Merger, ..) => "mergedBy".to_string(),
(Reviewer, _, Token(_)) => "reviewerWildcardId".to_string(),
......
use crate::types::{
Expression,
Field::{self, *},
FieldType::*,
Operator::{self, *},
RelationshipType::*,
Source,
Value::{self, *},
};
use crate::types::{Expression, Field, Field::*, Operator, Operator::*, Source, Value, Value::*};
use chrono::NaiveDate;
use indexmap::map::IndexMap;
......@@ -58,20 +50,25 @@ fn combine_values(a: &Value, b: &Value) -> Result<Value, String> {
}
pub fn expand_expression(expr: &Expression, source: &Source) -> Vec<Expression> {
let mut result = vec![expr.clone()];
if source.field_types(&expr.field).contains(&DateLike)
if source.field_type(&expr.field).is_date_like()
&& expr.operator == Equal
&& is_date_castable(&expr.value)
{
result.push(Expression::new(
expr.field.clone(),
LessThan,
concat_datelike_values(&expr.value, " 23:59"),
));
vec![
Expression::new(
expr.field.clone(),
GreaterThan,
concat_datelike_values(&expr.value, " 00:00"),
),
Expression::new(
expr.field.clone(),
LessThan,
concat_datelike_values(&expr.value, " 23:59"),
),
]
} else {
vec![expr.clone()]
}
result
}
fn is_falsey(v: &Value) -> bool {
......@@ -116,37 +113,29 @@ pub fn transform_expression(expr: &Expression, source: &Source) -> Expression {
let mut o = expr.operator.clone();
let mut v = expr.value.clone();
let field_types = source
.field_types(&f)
.iter()
.map(|t| match t {
WithOperators(t, _) => *t.clone(),
_ => t.clone(),
})
.collect::<Vec<_>>();
let field_type = source.field_type(&f);
let token = |v: &str| Token(v.into());
(f, o, v) = match (&f, &o, &v) {
(_, NotEqual, List(arr)) if arr.is_empty() => (f, Equal, token("ANY")),
(_, Equal, List(arr)) if arr.is_empty() => (f, o, token("NONE")),
(_, NotEqual, Bool(b)) => (f, Equal, Bool(!b)),
(Opened, GreaterThan | LessThan, _) => (Created, o, v),
(Opened, _, v) if is_falsey(v) => ("state".into(), Equal, token("closed")),
(Opened, _, Bool(_)) => ("state".into(), Equal, token("opened")),
(Opened, Equal, _) => (Created, GreaterThan, concat_datelike_values(&v, " 00:00")),
(Closed, _, v) if is_falsey(v) => ("state".into(), Equal, token("opened")),
(Closed, _, Bool(_)) => ("state".into(), Equal, token("closed")),
(.., Bool(_)) if field_types == vec![BooleanLike] => (f, o, v),
(..) if field_types == vec![BooleanLike] => (f, o, truthy(&v)),
(_, Equal, v) if field_types.contains(&DateLike) && is_date_castable(v) => {
(f, GreaterThan, concat_datelike_values(v, " 00:00"))
}
(_, In, List(_)) if field_types.contains(&ListLike(ManyToOne)) => (f, Equal, v),
(_, NotEqual, Token(w)) if field_types.contains(&Nullable) && w == "ANY" => {
(Closed, GreaterThan | LessThan, _) => (Closed, o, v),
(Opened, _, v) if is_falsey(v) => (State, Equal, token("closed")),
(Opened, _, Bool(_)) => (State, Equal, token("opened")),
(Closed, _, v) if is_falsey(v) => (State, Equal, token("opened")),
(Closed, _, Bool(_)) => (State, Equal, token("closed")),
(.., Bool(_)) if field_type.is_boolean_like() => (f, o, v),
(..) if field_type.is_boolean_like() => (f, o, truthy(&v)),
(_, In, List(_)) if field_type.is_has_one_list_like() => (f, Equal, v),
(_, NotEqual, Token(w)) if field_type.is_nullable() && w == "ANY" => {
(f, Equal, token("NONE"))
}
(_, NotEqual, Token(w)) if field_types.contains(&Nullable) && w == "NONE" => {
(_, NotEqual, Token(w)) if field_type.is_nullable() && w == "NONE" => {
(f, Equal, token("ANY"))
}
_ => (f, o, v),
......
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::BitOr;
use Field::*;
use FieldType::*;
use Operator::*;
use ReferenceType::*;
use RelationshipType::*;
use Source::*;
use TimeUnit::*;
use Value::*;
......@@ -105,8 +107,8 @@ impl From<&str> for Field {
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum RelationshipType {
ManyToMany,
ManyToOne,
HasMany,
HasOne,
}
#[derive(Debug, Eq, PartialEq, Clone)]
......@@ -116,20 +118,102 @@ pub enum FieldType {
NumberLike,
DateLike,
BooleanLike,
ListLike(RelationshipType),
ListLike(RelationshipType, Box<FieldType>),
EnumLike(Vec<String>),
StringEnumLike(Vec<String>),
ReferenceLike(ReferenceType),
WithOperators(Box<FieldType>, Vec<Operator>),
Multiple(Box<Vec<FieldType>>),
Unsupported,
}
impl FieldType {
pub fn with_operators(&self, operators: &[Operator]) -> FieldType {
pub fn is_boolean_like(&self) -> bool {
self.any(|f| matches!(f, BooleanLike))
}
pub fn is_date_like(&self) -> bool {
self.any(|f| matches!(f, DateLike))
}
pub fn is_nullable(&self) -> bool {
self.any(|f| matches!(f, Nullable))
}
pub fn is_list_like(&self) -> bool {
self.any(|f| matches!(f, ListLike(..)))
}
pub fn is_has_many_list_like(&self) -> bool {
self.any(|f| matches!(f, ListLike(r, _) if *r == HasMany))
}
pub fn is_has_one_list_like(&self) -> bool {
self.any(|f| matches!(f, ListLike(r, _) if *r == HasOne))
}
pub fn is_enum_like(&self) -> bool {
self.any(|f| matches!(f, EnumLike(..) | StringEnumLike(..)))
}
pub fn enum_values(&self) -> Vec<String> {
match self {
EnumLike(values) | StringEnumLike(values) => values.clone(),
_ => vec![],
}
}
pub fn find(&self, f: impl Fn(&FieldType) -> bool) -> Option<FieldType> {
match self {
WithOperators(t, _) => t.find(f),
Multiple(v) => v.iter().find(|t| t.any(&f)).cloned(),
t if f(t) => Some(t.clone()),
_ => None,
}
}
pub fn any(&self, f: impl Fn(&FieldType) -> bool) -> bool {
let mut types = vec![self];
while let Some(field_type) = types.pop() {
match field_type {
WithOperators(t, _) => types.push(t),
Multiple(vec) => types.extend(vec.iter()),
_ => {
if f(field_type) {
return true;
}
}
}
}
false
}
pub fn with_ops<const N: usize>(&self, operators: [Operator; N]) -> FieldType {
WithOperators(Box::new(self.to_owned()), operators.to_vec())
}
}
impl BitOr for FieldType {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
match (self, rhs) {
(Multiple(vec1), Multiple(vec2)) => {
let mut combined = vec1.to_vec();
combined.extend(vec2.to_vec());
Multiple(Box::new(combined))
}
(Multiple(vec), other) | (other, Multiple(vec)) => {
let mut combined = vec.to_vec();
combined.push(other);
Multiple(Box::new(combined))
}
(lhs, rhs) => Multiple(Box::new(vec![lhs, rhs])),
}
}
}
impl fmt::Display for FieldType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
......@@ -139,7 +223,7 @@ impl fmt::Display for FieldType {
DateLike => write!(f, "`Date`"),
BooleanLike => write!(f, "`Boolean` (`true`, `false`)"),
ReferenceLike(r) => write!(f, "{}", r),
ListLike(_) => write!(f, "`List`"),
ListLike(..) => write!(f, "`List`"),
EnumLike(v) => {
write!(
f,
......@@ -163,6 +247,8 @@ impl fmt::Display for FieldType {
// recursive
WithOperators(field, _) => write!(f, "{}", field),
Multiple(v) => v.iter().try_fold((), |_, field| write!(f, "{}", field)),
Unsupported => write!(f, "`Unsupported`"),
}
}
}
......
......@@ -31,3 +31,11 @@ fn test_id_equals_list() {
"Error: `id` does not support the equals (`=`) operator for `(1, 2, 3)`. Supported operators: is one of (`in`)."
)
}
#[test]
fn test_id_equals_string_list() {
assert_eq!(
compile_graphql("id = (\"1\", \"2\", \"3\")"),
"Error: `id` cannot be compared with `(\"1\", \"2\", \"3\")`. Supported value types: `Number`, `List`."
)
}
......@@ -124,44 +124,44 @@ fn test_null_with_additional_characters() {
#[test]
fn test_array_with_tokens() {
assert_eq!(
compile_graphql("label in (true, false)")
compile_graphql("label in (~true, ~false)")
.lines()
.nth(1)
.unwrap(),
"issues(or: { labelNames: [true,false] }, first: 100) {"
"issues(or: { labelNames: [\"true\",\"false\"] }, first: 100) {"
);
}
#[test]
fn test_array_with_spaces() {
assert_eq!(
compile_graphql("label in ( true , false )")
compile_graphql("label in ( ~true , ~false )")
.lines()
.nth(1)
.unwrap(),
"issues(or: { labelNames: [true,false] }, first: 100) {"
"issues(or: { labelNames: [\"true\",\"false\"] }, first: 100) {"
);
}
#[test]
fn test_leading_whitespace() {
assert_eq!(
compile_graphql(" \n label in (true, false)")
compile_graphql(" \n label in (~true, ~false)")
.lines()
.nth(1)
.unwrap(),
"issues(or: { labelNames: [true,false] }, first: 100) {"
"issues(or: { labelNames: [\"true\",\"false\"] }, first: 100) {"
);
}
#[test]
fn test_trailing_whitespace() {
assert_eq!(
compile_graphql("label in (true, false) \n ")
compile_graphql("label in (~true, ~false) \n ")
.lines()
.nth(1)
.unwrap(),
"issues(or: { labelNames: [true,false] }, first: 100) {"
"issues(or: { labelNames: [\"true\",\"false\"] }, first: 100) {"
);
}
......@@ -242,11 +242,11 @@ fn test_value_with_additional_characters_at_end_of_array_with_spaces() {
#[test]
fn test_array_with_tokens_with_bracket_spaces() {
assert_eq!(
compile_graphql("label in ( true, false )")
compile_graphql("label in ( ~true, ~false )")
.lines()
.nth(1)
.unwrap(),
"issues(or: { labelNames: [true,false] }, first: 100) {"
"issues(or: { labelNames: [\"true\",\"false\"] }, first: 100) {"
);
}
......
......@@ -26,7 +26,7 @@ fn test_type_equals_issue() {
.lines()
.nth(1)
.unwrap(),
"issues(type: ISSUE, state: opened, first: 100) {"
"issues(types: ISSUE, state: opened, first: 100) {"
);
assert_eq!(
......@@ -34,7 +34,7 @@ fn test_type_equals_issue() {
.lines()
.nth(1)
.unwrap(),
"issues(confidential: true, type: ISSUE, first: 100) {"
"issues(confidential: true, types: ISSUE, first: 100) {"
);
}
......@@ -45,7 +45,7 @@ fn test_type_equals_work_item_type() {
.lines()
.nth(1)
.unwrap(),
"issues(type: OBJECTIVE, state: opened, first: 100) {"
"issues(types: OBJECTIVE, state: opened, first: 100) {"
);
assert_eq!(
......@@ -53,7 +53,7 @@ fn test_type_equals_work_item_type() {
.lines()
.nth(1)
.unwrap(),
"issues(type: KEY_RESULT, state: opened, first: 100) {"
"issues(types: KEY_RESULT, state: opened, first: 100) {"
);
}
......@@ -61,12 +61,12 @@ fn test_type_equals_work_item_type() {
fn test_type_equals_invalid() {
assert_eq!(
compile_graphql("opened = true and type = none"),
"Error: `type` cannot be compared with `NONE`. Supported value types: `Enum` (`Issue`, `Incident`, `TestCase`, `Requirement`, `Task`, `Ticket`, `Objective`, `KeyResult`, `Epic`, `MergeRequest`)."
"Error: `type` cannot be compared with `NONE`. Supported value types: `Enum` (`Issue`, `Incident`, `TestCase`, `Requirement`, `Task`, `Ticket`, `Objective`, `KeyResult`, `Epic`, `MergeRequest`), `List`."
);
assert_eq!(
compile_graphql("blah = \"fish\" and type = none"),
"Error: `type` cannot be compared with `NONE`. Supported value types: `Enum` (`Issue`, `Incident`, `TestCase`, `Requirement`, `Task`, `Ticket`, `Objective`, `KeyResult`, `Epic`, `MergeRequest`)."
"Error: `type` cannot be compared with `NONE`. Supported value types: `Enum` (`Issue`, `Incident`, `TestCase`, `Requirement`, `Task`, `Ticket`, `Objective`, `KeyResult`, `Epic`, `MergeRequest`), `List`."
);
}
......@@ -82,3 +82,30 @@ fn test_type_equals_valid_but_invalid_fields() {
"Error: `confidential` is not a recognized field for merge requests."
)
}
#[test]
fn type_test_in_work_item_types() {
assert_eq!(
compile_graphql("type in (Issue, Incident, TestCase)")
.lines()
.nth(1)
.unwrap(),
"issues(types: [ISSUE,INCIDENT,TEST_CASE], first: 100) {"
);
assert_eq!(
compile_graphql("type in (Objective, KeyResult)")
.lines()
.nth(1)
.unwrap(),
"issues(types: [OBJECTIVE,KEY_RESULT], first: 100) {"
);
}
#[test]
fn type_test_in_invalid_type() {
assert_eq!(
compile_graphql("type in (MergeRequest, Issue)"),
"Error: `type` cannot be compared with `(MERGEREQUEST, ISSUE)`. Supported value types: `Enum` (`Issue`, `Incident`, `TestCase`, `Requirement`, `Task`, `Ticket`, `Objective`, `KeyResult`, `Epic`, `MergeRequest`), `List`."
);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment