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

Merge branch 'himkp-feat-subgroups' into 'main'

feat: add support for includeSubgroups param

See merge request !106
parents 88abf106 600600f1
No related branches found
No related tags found
No related merge requests found
Pipeline #1678108967 passed
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
exports[`graphql: compiles 'assignee = currentUser()' with default fields: ['id', 'title'] 1`] = ` exports[`graphql: compiles 'assignee = currentUser()' with default fields: ['id', 'title'] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues(assigneeUsernames: "foo", first: 10, includeSubgroups: true) { issues(assigneeUsernames: "foo", first: 10) {
nodes { nodes {
id id
title title
...@@ -27,7 +27,6 @@ exports[`graphql: compiles 'cadence = 123 and iteration = current' with default ...@@ -27,7 +27,6 @@ exports[`graphql: compiles 'cadence = 123 and iteration = current' with default
iterationCadenceId: "gid://gitlab/Iterations::Cadence/123" iterationCadenceId: "gid://gitlab/Iterations::Cadence/123"
iterationWildcardId: CURRENT iterationWildcardId: CURRENT
first: 10 first: 10
includeSubgroups: true
) { ) {
nodes { nodes {
id id
...@@ -48,7 +47,7 @@ exports[`graphql: compiles 'cadence = 123 and iteration = current' with default ...@@ -48,7 +47,7 @@ exports[`graphql: compiles 'cadence = 123 and iteration = current' with default
exports[`graphql: compiles 'health = "at risk"' with default fields: ['id', 'title'] 1`] = ` exports[`graphql: compiles 'health = "at risk"' with default fields: ['id', 'title'] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues(healthStatusFilter: atRisk, first: 10, includeSubgroups: true) { issues(healthStatusFilter: atRisk, first: 10) {
nodes { nodes {
id id
title title
...@@ -68,7 +67,7 @@ exports[`graphql: compiles 'health = "at risk"' with default fields: ['id', 'tit ...@@ -68,7 +67,7 @@ exports[`graphql: compiles 'health = "at risk"' with default fields: ['id', 'tit
exports[`graphql: compiles 'health = "needs attention"' with default fields: ['id', 'title'] 1`] = ` exports[`graphql: compiles 'health = "needs attention"' with default fields: ['id', 'title'] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues(healthStatusFilter: needsAttention, first: 10, includeSubgroups: true) { issues(healthStatusFilter: needsAttention, first: 10) {
nodes { nodes {
id id
title title
...@@ -88,7 +87,7 @@ exports[`graphql: compiles 'health = "needs attention"' with default fields: ['i ...@@ -88,7 +87,7 @@ exports[`graphql: compiles 'health = "needs attention"' with default fields: ['i
exports[`graphql: compiles 'health = "on track"' with default fields: ['id', 'title'] 1`] = ` exports[`graphql: compiles 'health = "on track"' with default fields: ['id', 'title'] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues(healthStatusFilter: onTrack, first: 10, includeSubgroups: true) { issues(healthStatusFilter: onTrack, first: 10) {
nodes { nodes {
id id
title title
...@@ -108,7 +107,7 @@ exports[`graphql: compiles 'health = "on track"' with default fields: ['id', 'ti ...@@ -108,7 +107,7 @@ exports[`graphql: compiles 'health = "on track"' with default fields: ['id', 'ti
exports[`graphql: compiles 'iteration = 123' with default fields: ['id', 'title'] 1`] = ` exports[`graphql: compiles 'iteration = 123' with default fields: ['id', 'title'] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues(iterationId: 123, first: 10, includeSubgroups: true) { issues(iterationId: 123, first: 10) {
nodes { nodes {
id id
title title
...@@ -134,7 +133,6 @@ exports[`graphql: compiles 'label != "backend" and author = currentUser() and we ...@@ -134,7 +133,6 @@ exports[`graphql: compiles 'label != "backend" and author = currentUser() and we
updatedAfter: "2024-01-01" updatedAfter: "2024-01-01"
not: {labelName: "backend"} not: {labelName: "backend"}
first: 10 first: 10
includeSubgroups: true
) { ) {
nodes { nodes {
id id
...@@ -155,11 +153,7 @@ exports[`graphql: compiles 'label != "backend" and author = currentUser() and we ...@@ -155,11 +153,7 @@ exports[`graphql: compiles 'label != "backend" and author = currentUser() and we
exports[`graphql: compiles 'label in ("devops::plan", "devops::create")' with default fields: ['id', 'title'] 1`] = ` exports[`graphql: compiles 'label in ("devops::plan", "devops::create")' with default fields: ['id', 'title'] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues( issues(or: {labelNames: ["devops::plan", "devops::create"]}, first: 10) {
or: {labelNames: ["devops::plan", "devops::create"]}
first: 10
includeSubgroups: true
) {
nodes { nodes {
id id
title title
...@@ -179,7 +173,7 @@ exports[`graphql: compiles 'label in ("devops::plan", "devops::create")' with de ...@@ -179,7 +173,7 @@ exports[`graphql: compiles 'label in ("devops::plan", "devops::create")' with de
exports[`graphql: compiles 'updated > today()' with default fields: ['id', 'title'] 1`] = ` exports[`graphql: compiles 'updated > today()' with default fields: ['id', 'title'] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues(updatedAfter: "2024-01-01", first: 10, includeSubgroups: true) { issues(updatedAfter: "2024-01-01", first: 10) {
nodes { nodes {
id id
title title
...@@ -199,12 +193,7 @@ exports[`graphql: compiles 'updated > today()' with default fields: ['id', 'titl ...@@ -199,12 +193,7 @@ exports[`graphql: compiles 'updated > today()' with default fields: ['id', 'titl
exports[`graphql: compiles 'weight = 1 and updated > startOfDay("-7")' with default fields: ['id', 'title'] 1`] = ` exports[`graphql: compiles 'weight = 1 and updated > startOfDay("-7")' with default fields: ['id', 'title'] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues( issues(weight: "1", updatedAfter: "2023-12-25", first: 10) {
weight: "1"
updatedAfter: "2023-12-25"
first: 10
includeSubgroups: true
) {
nodes { nodes {
id id
title title
...@@ -224,7 +213,7 @@ exports[`graphql: compiles 'weight = 1 and updated > startOfDay("-7")' with defa ...@@ -224,7 +213,7 @@ exports[`graphql: compiles 'weight = 1 and updated > startOfDay("-7")' with defa
exports[`graphql: compiles 'weight = 1' with default fields: ['id', 'title'] 1`] = ` exports[`graphql: compiles 'weight = 1' with default fields: ['id', 'title'] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues(weight: "1", first: 10, includeSubgroups: true) { issues(weight: "1", first: 10) {
nodes { nodes {
id id
title title
...@@ -244,7 +233,7 @@ exports[`graphql: compiles 'weight = 1' with default fields: ['id', 'title'] 1`] ...@@ -244,7 +233,7 @@ exports[`graphql: compiles 'weight = 1' with default fields: ['id', 'title'] 1`]
exports[`graphql: compiles query with custom fields: ["id", "assignees"] 1`] = ` exports[`graphql: compiles query with custom fields: ["id", "assignees"] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues(weight: "1", first: 10, includeSubgroups: true) { issues(weight: "1", first: 10) {
nodes { nodes {
id id
assignees { assignees {
...@@ -272,7 +261,7 @@ exports[`graphql: compiles query with custom fields: ["id", "assignees"] 1`] = ` ...@@ -272,7 +261,7 @@ exports[`graphql: compiles query with custom fields: ["id", "assignees"] 1`] = `
exports[`graphql: compiles query with custom fields: ["id", "iteration"] 1`] = ` exports[`graphql: compiles query with custom fields: ["id", "iteration"] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues(weight: "1", first: 10, includeSubgroups: true) { issues(weight: "1", first: 10) {
nodes { nodes {
id id
iteration { iteration {
...@@ -303,7 +292,7 @@ exports[`graphql: compiles query with custom fields: ["id", "iteration"] 1`] = ` ...@@ -303,7 +292,7 @@ exports[`graphql: compiles query with custom fields: ["id", "iteration"] 1`] = `
exports[`graphql: compiles query with custom fields: ["id", "labels"] 1`] = ` exports[`graphql: compiles query with custom fields: ["id", "labels"] 1`] = `
"query GLQL { "query GLQL {
group(fullPath: "gitlab-org") { group(fullPath: "gitlab-org") {
issues(weight: "1", first: 10, includeSubgroups: true) { issues(weight: "1", first: 10) {
nodes { nodes {
id id
labels { labels {
...@@ -335,7 +324,6 @@ exports[`graphql: compiles with cursor after 1`] = ` ...@@ -335,7 +324,6 @@ exports[`graphql: compiles with cursor after 1`] = `
updatedAfter: "2023-12-25" updatedAfter: "2023-12-25"
after: "eyJpZCI6IjEifQ==" after: "eyJpZCI6IjEifQ=="
first: 10 first: 10
includeSubgroups: true
) { ) {
nodes { nodes {
id id
...@@ -361,7 +349,6 @@ exports[`graphql: compiles with cursor before 1`] = ` ...@@ -361,7 +349,6 @@ exports[`graphql: compiles with cursor before 1`] = `
updatedAfter: "2023-12-25" updatedAfter: "2023-12-25"
before: "eyJpZCI6IjEifQ==" before: "eyJpZCI6IjEifQ=="
first: 10 first: 10
includeSubgroups: true
) { ) {
nodes { nodes {
id id
......
...@@ -35,6 +35,24 @@ impl Field { ...@@ -35,6 +35,24 @@ impl Field {
source.validate_field(self) source.validate_field(self)
} }
pub fn validate_paired_fields(
&self,
fields: Vec<Field>,
source: &Source,
) -> Result<(), GlqlError> {
let pairable_fields = source.field_type(self).pairable_fields();
match fields
.iter()
.any(|f| pairable_fields.contains(f) || pairable_fields.is_empty())
{
true => Ok(()),
false => Err(GlqlError::analyzer_error(IncorrectFieldPairing {
field: self.clone(),
supported_fields: pairable_fields,
})),
}
}
pub fn validate_operator(&self, op: &Operator, source: &Source) -> Result<(), GlqlError> { pub fn validate_operator(&self, op: &Operator, source: &Source) -> Result<(), GlqlError> {
let ops = self.operators(source); let ops = self.operators(source);
let supported_operators = ops.iter().cloned().collect::<Vec<_>>(); let supported_operators = ops.iter().cloned().collect::<Vec<_>>();
......
use crate::types::{ use crate::types::{
FieldType, FieldType::*, Operator, Operator::*, RelationshipType::*, Value, Value::*, Field, FieldType, FieldType::*, Operator, Operator::*, RelationshipType::*, Value, Value::*,
}; };
use chrono::NaiveDate; use chrono::NaiveDate;
use indexmap::set::IndexSet; use indexmap::set::IndexSet;
...@@ -40,23 +40,35 @@ impl FieldType { ...@@ -40,23 +40,35 @@ impl FieldType {
.contains(&v.to_lowercase()), .contains(&v.to_lowercase()),
_ => false, _ => false,
}, },
WithOperators(field, _) => field.validate(value), WithOperators(field, _) | PairedWith(field, _) => field.validate(value),
Multiple(fields) => fields.iter().any(|f| f.validate(value)), Multiple(fields) => fields.iter().any(|f| f.validate(value)),
Unsupported => false, Unsupported => false,
} }
} }
pub fn pairable_fields(&self) -> Vec<Field> {
match self {
Multiple(f) => f
.iter()
.flat_map(|f| f.pairable_fields())
.collect::<Vec<_>>(),
WithOperators(f, _) => f.pairable_fields(),
PairedWith(_, f) => f.to_vec(),
_ => vec![],
}
}
pub fn operators(&self) -> IndexSet<Operator> { pub fn operators(&self) -> IndexSet<Operator> {
match self { match self {
Nullable | StringLike | NumberLike | BooleanLike | EnumLike(_) | StringEnumLike(_) Nullable | StringLike | NumberLike | BooleanLike | EnumLike(_) | StringEnumLike(_)
| ReferenceLike(..) => vec![Equal, NotEqual], | ReferenceLike(..) => vec![Equal, NotEqual].into_iter().collect(),
WithOperators(_, ops) => ops.clone(), WithOperators(_, ops) => ops.clone().into_iter().collect(),
DateLike => vec![Equal, GreaterThan, LessThan], DateLike => vec![Equal, GreaterThan, LessThan].into_iter().collect(),
ListLike(HasOne, _) => vec![In, NotEqual], ListLike(HasOne, _) => vec![In, NotEqual].into_iter().collect(),
ListLike(HasMany, _) => vec![In, Equal, NotEqual], ListLike(HasMany, _) => vec![In, Equal, NotEqual].into_iter().collect(),
Multiple(_) | Unsupported => vec![], PairedWith(f, _) => f.operators(),
Multiple(_) | Unsupported => vec![].into_iter().collect(),
} }
.into_iter()
.collect()
} }
} }
use crate::errors::{GlqlError, GlqlErrorKind::*}; use crate::errors::{GlqlError, GlqlErrorKind::*};
use crate::transformer::context::remove_extracted_fields;
use crate::{ use crate::{
parser::ParsedQuery, parser::ParsedQuery,
transformer::transform, transformer::transform,
types::{Context, Field::*, Query, Source}, types::{Context, Query, Source},
}; };
pub mod expression; pub mod expression;
...@@ -28,9 +29,14 @@ impl TryFrom<ParsedQuery> for ValidatedQuery { ...@@ -28,9 +29,14 @@ impl TryFrom<ParsedQuery> for ValidatedQuery {
type Error = GlqlError; type Error = GlqlError;
fn try_from(parsed_query: ParsedQuery) -> Result<Self, Self::Error> { fn try_from(parsed_query: ParsedQuery) -> Result<Self, Self::Error> {
analyze(parsed_query.query(), parsed_query.context())?; let mut query = parsed_query.query().clone();
let valid_query = transform(parsed_query.query(), parsed_query.context()); let context = parsed_query.context();
Ok(Self(valid_query, parsed_query.context().clone()))
analyze(&query, context)?;
remove_extracted_fields(&mut query, context);
let valid_query = transform(&query, context);
Ok(Self(valid_query, context.clone()))
} }
} }
...@@ -46,27 +52,10 @@ fn context_source(context: &Context) -> Result<&Source, GlqlError> { ...@@ -46,27 +52,10 @@ fn context_source(context: &Context) -> Result<&Source, GlqlError> {
.ok_or(GlqlError::analyzer_error(SourceNotProvided)) .ok_or(GlqlError::analyzer_error(SourceNotProvided))
} }
pub fn analyze_context_fields(query: &Query, context: &Context) -> Result<(), GlqlError> {
let context_fields = &[Type, Group, Project];
let expressions = query
.expressions
.iter()
.filter(|exp| context_fields.contains(&exp.field))
.collect::<Vec<_>>();
let source = context_source(context)?;
for exp in &expressions {
exp.field.validate(&exp.operator, &exp.value, source)?
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::types::{Expression, Operator::*, Value::*}; use crate::types::{Expression, Field::*, Operator::*, Value::*};
#[test] #[test]
fn test_validate_invalid_field() { fn test_validate_invalid_field() {
......
...@@ -7,11 +7,17 @@ impl Query { ...@@ -7,11 +7,17 @@ impl Query {
self.validate_expressions(source) self.validate_expressions(source)
} }
pub fn validate_expressions(&self, source: &Source) -> Result<(), GlqlError> { fn validate_expressions(&self, source: &Source) -> Result<(), GlqlError> {
let mut in_operators = IndexSet::new(); let mut in_operators = IndexSet::new();
let fields = self
.expressions
.iter()
.map(|expr| expr.field.clone())
.collect::<Vec<_>>();
for expr in &self.expressions { for expr in &self.expressions {
expr.validate(source)?; expr.validate(source)?;
expr.field.validate_paired_fields(fields.clone(), source)?;
if expr.operator == In && !in_operators.insert(expr.field.clone()) { if expr.operator == In && !in_operators.insert(expr.field.clone()) {
Err(GlqlError::analyzer_error(DuplicateOperatorForField { Err(GlqlError::analyzer_error(DuplicateOperatorForField {
......
...@@ -25,6 +25,7 @@ pub fn is_valid_field(field: &Field) -> bool { ...@@ -25,6 +25,7 @@ pub fn is_valid_field(field: &Field) -> bool {
| Iteration | Iteration
| Cadence | Cadence
| Due | Due
| IncludeSubgroups
) )
} }
...@@ -82,6 +83,7 @@ pub fn field_type(field: &Field) -> FieldType { ...@@ -82,6 +83,7 @@ pub fn field_type(field: &Field) -> FieldType {
Created | Updated | Due => DateLike, Created | Updated | Due => DateLike,
Weight => NumberLike | Nullable, Weight => NumberLike | Nullable,
Confidential => BooleanLike, Confidential => BooleanLike,
IncludeSubgroups => BooleanLike.paired_with([Group]),
Health => { Health => {
StringEnumLike(vec![ StringEnumLike(vec![
"on track".into(), "on track".into(),
......
...@@ -23,6 +23,7 @@ pub fn is_valid_field(field: &Field) -> bool { ...@@ -23,6 +23,7 @@ pub fn is_valid_field(field: &Field) -> bool {
| Deployed | Deployed
| Draft | Draft
| Environment | Environment
| IncludeSubgroups
) )
} }
...@@ -66,6 +67,7 @@ pub fn field_type(field: &Field) -> FieldType { ...@@ -66,6 +67,7 @@ pub fn field_type(field: &Field) -> FieldType {
Environment | Group | Project => StringLike.with_ops([Equal]), Environment | Group | Project => StringLike.with_ops([Equal]),
Created | Updated | Merged | Deployed => DateLike, Created | Updated | Merged | Deployed => DateLike,
Draft => BooleanLike, Draft => BooleanLike,
IncludeSubgroups => BooleanLike.paired_with([Group]),
_ => Unsupported, _ => Unsupported,
} }
} }
...@@ -179,12 +179,6 @@ pub fn to_graphql_components(query: &Query, context: &Context) -> GraphQLCompone ...@@ -179,12 +179,6 @@ pub fn to_graphql_components(query: &Query, context: &Context) -> GraphQLCompone
.insert("first".to_string(), Number(limit as i64)); .insert("first".to_string(), Number(limit as i64));
} }
if context.group.is_some() {
components
.constraints
.insert("includeSubgroups".to_string(), Bool(true));
}
if query.expressions.iter().any(|expr| expr.field == Epic) { if query.expressions.iter().any(|expr| expr.field == Epic) {
components components
.constraints .constraints
...@@ -444,10 +438,6 @@ mod tests { ...@@ -444,10 +438,6 @@ mod tests {
// Check that the components are correctly generated based on the input // Check that the components are correctly generated based on the input
let mut valid = true; let mut valid = true;
// Check constraints
if test_case.context.group.is_some() {
valid &= components.constraints.get("includeSubgroups") == Some(&Bool(true));
}
if let Some(limit) = test_case.context.limit { if let Some(limit) = test_case.context.limit {
valid &= components.constraints.get("first") == Some(&Number(limit as i64)); valid &= components.constraints.get("first") == Some(&Number(limit as i64));
} }
......
...@@ -64,6 +64,10 @@ pub enum GlqlErrorKind { ...@@ -64,6 +64,10 @@ pub enum GlqlErrorKind {
value: Value, value: Value,
supported_value_types: Vec<FieldType>, supported_value_types: Vec<FieldType>,
}, },
IncorrectFieldPairing {
field: Field,
supported_fields: Vec<Field>,
},
} }
impl GlqlError { impl GlqlError {
...@@ -188,6 +192,18 @@ impl fmt::Display for GlqlError { ...@@ -188,6 +192,18 @@ impl fmt::Display for GlqlError {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
), ),
IncorrectFieldPairing {
field,
supported_fields,
} => format!(
"`{}` can only be used with: {}.",
field,
supported_fields
.iter()
.map(|f| format!("`{}`", f))
.collect::<Vec<_>>()
.join(", ")
),
}; };
write!(f, "Error: {}", error) write!(f, "Error: {}", error)
......
use crate::analyzer::analyze_context_fields;
use crate::errors::{GlqlError, GlqlErrorKind::*}; use crate::errors::{GlqlError, GlqlErrorKind::*};
use crate::transformer::context::extract_context; use crate::transformer::context::extract_context;
use crate::types::{Fields, Query}; use crate::types::{Fields, Query};
...@@ -28,8 +27,7 @@ impl ParsedQuery { ...@@ -28,8 +27,7 @@ impl ParsedQuery {
pub fn parse(input: &str, ctx: &mut Context) -> Result<Self, GlqlError> { pub fn parse(input: &str, ctx: &mut Context) -> Result<Self, GlqlError> {
let mut query = parse_query(input)?; let mut query = parse_query(input)?;
apply_functions(&mut query, ctx)?; apply_functions(&mut query, ctx)?;
analyze_context_fields(&query, ctx)?; extract_context(&query, ctx);
extract_context(&mut query, ctx);
Ok(Self(query, ctx.clone())) Ok(Self(query, ctx.clone()))
} }
......
use crate::types::{Context, Field::*, Query, Source::*, Value::*}; use crate::types::{Context, Field::*, Query, Source::*, Value::*};
pub fn extract_context(query: &mut Query, context: &mut Context) { pub fn remove_extracted_fields(query: &mut Query, context: &Context) {
query.expressions.retain(|expr| { query.expressions.retain(|expr| match &expr.field {
Type => context.source == Some(Issues),
Group | Project => false,
_ => true,
});
}
pub fn extract_context(query: &Query, context: &mut Context) {
for expr in &query.expressions {
match (&expr.field, &expr.value) { match (&expr.field, &expr.value) {
(Type, Token(value)) => { (Type, Token(value)) => {
context.source = Some(value.clone().into()); context.source = value.clone().try_into().ok();
} }
(Group, Quoted(value)) => { (Group, Quoted(value)) => {
context.group = Some(value.clone()); context.group = Some(value.clone());
...@@ -16,13 +24,7 @@ pub fn extract_context(query: &mut Query, context: &mut Context) { ...@@ -16,13 +24,7 @@ pub fn extract_context(query: &mut Query, context: &mut Context) {
} }
_ => {} _ => {}
} }
}
match &expr.field {
Type => context.source == Some(Issues),
Group | Project => false,
_ => true,
}
});
} }
#[cfg(test)] #[cfg(test)]
...@@ -46,8 +48,6 @@ mod tests { ...@@ -46,8 +48,6 @@ mod tests {
assert_eq!(context.project, Some("test-project".to_string())); assert_eq!(context.project, Some("test-project".to_string()));
// group is nullified // group is nullified
assert_eq!(context.group, None); assert_eq!(context.group, None);
assert_eq!(query.expressions.len(), 1);
assert_eq!(query.expressions[0].field, "status".into());
} }
#[test] #[test]
...@@ -60,7 +60,8 @@ mod tests { ...@@ -60,7 +60,8 @@ mod tests {
let mut context = Context::default(); let mut context = Context::default();
context.project = Some("gitlab-project".to_string()); context.project = Some("gitlab-project".to_string());
extract_context(&mut query, &mut context); extract_context(&query, &mut context);
remove_extracted_fields(&mut query, &context);
assert_eq!(context.group, Some("test-group".to_string())); assert_eq!(context.group, Some("test-group".to_string()));
// preferred project is nullified // preferred project is nullified
...@@ -82,7 +83,8 @@ mod tests { ...@@ -82,7 +83,8 @@ mod tests {
let mut context = Context::default(); let mut context = Context::default();
extract_context(&mut query, &mut context); extract_context(&query, &mut context);
remove_extracted_fields(&mut query, &context);
assert_eq!(context.group, None); assert_eq!(context.group, None);
assert_eq!(context.project, None); assert_eq!(context.project, None);
...@@ -98,7 +100,8 @@ mod tests { ...@@ -98,7 +100,8 @@ mod tests {
let mut context = Context::default(); let mut context = Context::default();
extract_context(&mut query, &mut context); extract_context(&query, &mut context);
remove_extracted_fields(&mut query, &context);
assert_eq!(context.group, None); assert_eq!(context.group, None);
assert_eq!(context.project, None); assert_eq!(context.project, None);
...@@ -114,7 +117,8 @@ mod tests { ...@@ -114,7 +117,8 @@ mod tests {
let mut context = Context::default(); let mut context = Context::default();
extract_context(&mut query, &mut context); extract_context(&query, &mut context);
remove_extracted_fields(&mut query, &context);
// type param is retained // type param is retained
assert_eq!(context.source, Some(Issues)); assert_eq!(context.source, Some(Issues));
...@@ -136,7 +140,8 @@ mod tests { ...@@ -136,7 +140,8 @@ mod tests {
let mut context = Context::default(); let mut context = Context::default();
extract_context(&mut query, &mut context); extract_context(&query, &mut context);
remove_extracted_fields(&mut query, &context);
// type param is extracted // type param is extracted
assert_eq!(context.source, Some(MergeRequests)); assert_eq!(context.source, Some(MergeRequests));
...@@ -156,7 +161,8 @@ mod tests { ...@@ -156,7 +161,8 @@ mod tests {
let mut context = Context::default(); let mut context = Context::default();
extract_context(&mut query, &mut context); extract_context(&query, &mut context);
remove_extracted_fields(&mut query, &context);
assert_eq!(context.source, Some(MergeRequests)); assert_eq!(context.source, Some(MergeRequests));
assert_eq!( assert_eq!(
......
...@@ -33,6 +33,7 @@ pub enum Field { ...@@ -33,6 +33,7 @@ pub enum Field {
Iteration, Iteration,
Cadence, Cadence,
Due, Due,
IncludeSubgroups,
Draft, Draft,
Approver, Approver,
...@@ -69,6 +70,7 @@ impl From<String> for Field { ...@@ -69,6 +70,7 @@ impl From<String> for Field {
"cadence" => Cadence, "cadence" => Cadence,
"due" => Due, "due" => Due,
"opened" => Opened, "opened" => Opened,
"includesubgroups" => IncludeSubgroups,
"reviewer" => Reviewer, "reviewer" => Reviewer,
"merger" => Merger, "merger" => Merger,
...@@ -88,6 +90,7 @@ impl From<&Field> for String { ...@@ -88,6 +90,7 @@ impl From<&Field> for String {
fn from(field: &Field) -> Self { fn from(field: &Field) -> Self {
match field { match field {
Unknown(s) => s.clone(), Unknown(s) => s.clone(),
IncludeSubgroups => "includeSubgroups".into(),
_ => format!("{:?}", field).to_lowercase(), _ => format!("{:?}", field).to_lowercase(),
} }
} }
...@@ -123,6 +126,7 @@ pub enum FieldType { ...@@ -123,6 +126,7 @@ pub enum FieldType {
StringEnumLike(Vec<String>), StringEnumLike(Vec<String>),
ReferenceLike(ReferenceType), ReferenceLike(ReferenceType),
PairedWith(Box<FieldType>, Vec<Field>),
WithOperators(Box<FieldType>, Vec<Operator>), WithOperators(Box<FieldType>, Vec<Operator>),
Multiple(Box<Vec<FieldType>>), Multiple(Box<Vec<FieldType>>),
Unsupported, Unsupported,
...@@ -192,6 +196,10 @@ impl FieldType { ...@@ -192,6 +196,10 @@ impl FieldType {
pub fn with_ops<const N: usize>(&self, operators: [Operator; N]) -> FieldType { pub fn with_ops<const N: usize>(&self, operators: [Operator; N]) -> FieldType {
WithOperators(Box::new(self.to_owned()), operators.to_vec()) WithOperators(Box::new(self.to_owned()), operators.to_vec())
} }
pub fn paired_with<const N: usize>(&self, fields: [Field; N]) -> FieldType {
PairedWith(Box::new(self.to_owned()), fields.to_vec())
}
} }
impl BitOr for FieldType { impl BitOr for FieldType {
...@@ -246,7 +254,7 @@ impl fmt::Display for FieldType { ...@@ -246,7 +254,7 @@ impl fmt::Display for FieldType {
} }
// recursive // recursive
WithOperators(field, _) => write!(f, "{}", field), WithOperators(field, _) | PairedWith(field, _) => write!(f, "{}", field),
Multiple(v) => v.iter().try_fold((), |_, field| write!(f, "{}", field)), Multiple(v) => v.iter().try_fold((), |_, field| write!(f, "{}", field)),
Unsupported => write!(f, "`Unsupported`"), Unsupported => write!(f, "`Unsupported`"),
} }
...@@ -398,8 +406,8 @@ impl From<String> for Source { ...@@ -398,8 +406,8 @@ impl From<String> for Source {
"issue" | "incident" | "testcase" | "requirement" | "task" | "ticket" | "objective" "issue" | "incident" | "testcase" | "requirement" | "task" | "ticket" | "objective"
| "keyresult" | "epic" => Issues, | "keyresult" | "epic" => Issues,
"mergerequest" => MergeRequests, "mergerequest" => MergeRequests,
// This should be unreachable due to earlier validation
_ => unreachable!("Unknown source: {}", s), _ => Issues,
} }
} }
} }
......
...@@ -24,7 +24,34 @@ fn test_group_scope() { ...@@ -24,7 +24,34 @@ fn test_group_scope() {
let result = compile_graphql("group=\"gitlab-org\""); let result = compile_graphql("group=\"gitlab-org\"");
let lines: Vec<&str> = result.lines().collect(); let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines[1], "group(fullPath: \"gitlab-org\") {"); assert_eq!(lines[1], "group(fullPath: \"gitlab-org\") {");
assert_eq!(lines[2], "issues(first: 100, includeSubgroups: true) {"); assert_eq!(lines[2], "issues(first: 100) {");
}
#[test]
fn test_include_subgroups() {
assert_eq!(
compile_graphql("group = \"gitlab-org\" and includeSubgroups = true")
.lines()
.nth(2)
.unwrap(),
"issues(includeSubgroups: true, first: 100) {"
);
}
#[test]
fn test_invalid_include_subgroups_with_project() {
assert_eq!(
compile_graphql("project = \"gitlab-org/gitlab\" and includeSubgroups = true"),
"Error: `includeSubgroups` can only be used with: `group`."
);
}
#[test]
fn test_include_subgroup_invalid_value() {
assert_eq!(
compile_graphql("group=\"gitlab-org\" and includeSubgroups = \"wrong\""),
"Error: `includeSubgroups` cannot be compared with `\"wrong\"`. Supported value types: `Boolean` (`true`, `false`)."
)
} }
#[test] #[test]
......
...@@ -65,7 +65,7 @@ fn test_type_equals_invalid() { ...@@ -65,7 +65,7 @@ fn test_type_equals_invalid() {
); );
assert_eq!( assert_eq!(
compile_graphql("blah = \"fish\" and type = none"), compile_graphql("label = \"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`), `List`." "Error: `type` cannot be compared with `NONE`. 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