Loading src/analyzer/sources/issues.rs +3 −2 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ impl SourceAnalyzer for IssuesSourceAnalyzer { | Status | Label | Epic | Parent | Created | Updated | Assignee Loading Loading @@ -114,8 +115,8 @@ impl SourceAnalyzer for IssuesSourceAnalyzer { | EnumLike(vec!["current".into()]) | ReferenceLike(IterationRef) } Epic => { // Use the new field mapping abstraction for Epic field Epic | Parent => { // Use the new field mapping abstraction for Epic and Parent fields let context = Context { source: Some(crate::types::Source::Issues), ..Context::default() Loading src/codegen/graphql_filters.rs +34 −2 Original line number Diff line number Diff line Loading @@ -85,7 +85,7 @@ impl GraphQLFilters { constraints.insert("excludeProjects".to_string(), Bool(true)); } if query.contains_field(&Epic) { if query.contains_field(&Epic) || query.contains_field(&Parent) { constraints.insert( match FeatureFlag::GlqlWorkItems.get() { true => "includeDescendantWorkItems".to_string(), Loading Loading @@ -217,7 +217,7 @@ fn to_graphql_filter_value(expr: &Expression, context: &mut Context) -> Value { (Assignee, Number(n)) => Quoted(n.to_string()), (Label | Approver, Token(s)) => Quoted(s.clone()), (Label | Assignee | Approver, Null) => Quoted("NONE".to_string()), (Weight | Epic, Null) => Token("NONE".to_string()), (Weight | Epic | Parent, Null) => Token("NONE".to_string()), (Weight, Number(n)) => Quoted(n.to_string()), (State, Token(s)) => Token(s.to_lowercase()), (Status, Quoted(s)) => Token(format!("{{name: \"{s}\"}}")), Loading Loading @@ -257,6 +257,27 @@ fn to_graphql_filter_value(expr: &Expression, context: &mut Context) -> Value { v.clone() } } (Parent, v) => { if let Some(mapping) = get_field_mapping(&Parent, context) { use crate::field_mapping::transform_epic_subquery_with_variable_type; let subquery = transform_epic_subquery_with_variable_type( &Parent, v, context, &mapping.variable_type, ); // graphql variables are not automatically coerced to lists, // so we need to wrap the subquery in a list, if it isn't already if matches!(subquery, Subquery(..)) { List(vec![subquery]) } else { subquery } } else { v.clone() } } (_, v) => v.clone(), } } Loading Loading @@ -335,6 +356,17 @@ fn to_graphql_filter_key(expr: &Expression, context: &Context) -> String { } } } (Parent, _, _) => { if let Some(mapping) = get_field_mapping(&Parent, context) { (mapping.get_attribute)(&mapping.field_type, &expr.value) } else { // Fallback match &expr.value { Null | Token(_) => "parentWildcardId".to_string(), _ => "parentIds".to_string(), } } } (Label, In, _) => pluralize_attribute("labelName").to_string(), (Label, _, _) => "labelName".to_string(), (Health, _, _) => "healthStatusFilter".to_string(), Loading src/field_mapping.rs +86 −25 Original line number Diff line number Diff line Loading @@ -3,7 +3,6 @@ use crate::types::{ Context, Field, Field::*, FieldMapping, FieldType, FieldType::*, ReferenceType::*, RelationshipType::*, Source::*, Value, }; use crate::utils::common::parse_epic_reference; use crate::utils::feature_flags::FeatureFlag; /// Attribute function for work items epic mapping (parent work items) Loading @@ -19,6 +18,19 @@ fn epic_work_items_attribute(_field_type: &FieldType, value: &Value) -> String { } } /// Attribute function for parent work items mapping fn parent_work_items_attribute(_field_type: &FieldType, value: &Value) -> String { use crate::types::Value::*; // For parent field, wildcard values (null/tokens) use wildcard attribute // All other values (including lists) use parentIds if matches!(value, Null | Token(_)) { "parentWildcardId".to_string() } else { "parentIds".to_string() } } /// Attribute function for legacy epic mapping fn epic_legacy_attribute(field_type: &FieldType, value: &Value) -> String { use crate::types::Value::*; Loading Loading @@ -58,13 +70,25 @@ pub fn get_field_mapping(field: &Field, context: &Context) -> Option<FieldMappin get_attribute: epic_legacy_attribute, }), // Future: Parent field implementation would go here // (Field::Parent, true, Some(Issues)) => Some(FieldMapping { // field_type: StringLike | NumberLike | ReferenceLike(WorkItemRef) | // ListLike(HasOne, Box::new(StringLike | NumberLike | ReferenceLike(WorkItemRef))), // variable_type: "WorkItemID!".to_string(), // get_attribute: parent_work_items_attribute, // Different logic - no group derivation // }), // Parent field with work items API (Field::Parent, true, Some(Issues)) => Some(FieldMapping { field_type: StringLike | NumberLike | ReferenceLike(WorkItemRef) | ReferenceLike(EpicRef) | ListLike( HasOne, Box::new( StringLike | NumberLike | ReferenceLike(WorkItemRef) | ReferenceLike(EpicRef), ), ), variable_type: "WorkItemID!".to_string(), get_attribute: parent_work_items_attribute, }), _ => None, } } Loading @@ -72,40 +96,51 @@ pub fn get_field_mapping(field: &Field, context: &Context) -> Option<FieldMappin /// Transforms a value for work items epic references /// Handles epic references like "&123" or "group&123" and generates work items API subqueries pub fn transform_to_work_item_epic_subquery(v: &Value, context: &mut Context) -> Value { transform_epic_subquery_with_variable_type(v, context, "WorkItemID!") transform_epic_subquery_with_variable_type(&Epic, v, context, "WorkItemID!") } /// Transforms a value for legacy epic references /// Handles epic references for legacy API and generates legacy epics API subqueries pub fn transform_to_legacy_epic_subquery(v: &Value, context: &mut Context) -> Value { transform_epic_subquery_with_variable_type(v, context, "String") transform_epic_subquery_with_variable_type(&Epic, v, context, "String") } /// Common epic subquery transformation logic with configurable variable type fn transform_epic_subquery_with_variable_type( pub fn transform_epic_subquery_with_variable_type( field: &Field, v: &Value, context: &mut Context, variable_type: &str, ) -> Value { use crate::types::{Value::*, Variable}; use crate::utils::common::unique_id; use crate::utils::common::{parse_work_item_reference, unique_id}; use std::path::Path; let id = match v { Number(n) => n.to_string(), Quoted(s) => s.clone(), Reference(_, s) => s.clone(), Reference(ref_type, s) => { let id_str = s.clone(); // Add prefix if not present match ref_type { EpicRef if !id_str.contains('&') => format!("&{}", id_str), WorkItemRef if !id_str.contains('#') => format!("#{}", id_str), _ => id_str, } } List(arr) => { return List( arr.iter() .map(|v| transform_epic_subquery_with_variable_type(v, context, variable_type)) .map(|v| { transform_epic_subquery_with_variable_type(field, v, context, variable_type) }) .collect(), ); } _ => return v.clone(), }; let epic_group = match &context.group { let parent_group = match &context.group { Some(group) => group.clone(), None => { let project_path = context.project.clone().unwrap_or("".to_string()); Loading @@ -126,27 +161,53 @@ fn transform_epic_subquery_with_variable_type( ..Context::default() }; let variable_key = unique_id("epicId"); let default_query = |group: &str, id: &str| { format!("type = Epic AND id = {id} AND includeSubgroups = false AND group = \"{group}\"") }; let is_epic = matches!(field.dealias(), Epic); let variable_key = unique_id(if is_epic { "epicId" } else { "parentId" }); let subquery = |query| { let subquery = |query: String| { context.variables.push( Variable::new(&variable_key, variable_type) .with_value(&compile_glql(query, &mut subquery_context).output), .with_value(&compile_glql(&query, &mut subquery_context).output), ); Subquery( query.to_string(), query, Variable::new(&variable_key, variable_type), Box::new(subquery_context), ) }; // Parse the epic reference using the utility function let (group_path, epic_id) = parse_epic_reference(&id, &epic_group); subquery(&default_query(&group_path, &epic_id)) // Parse the work item reference using the utility function if is_epic { use crate::utils::common::parse_epic_reference; let (group_path, epic_id) = parse_epic_reference(&id, &parent_group); subquery(format!( "type = Epic AND id = {epic_id} AND includeSubgroups = false AND group = \"{group_path}\"" )) } else { // Parent field - can be either epic or work item reference let (group_path, project_path, work_item_id) = parse_work_item_reference(&id, &parent_group); let query = if let Some(proj_path) = project_path { if !proj_path.is_empty() { format!("id = {work_item_id} AND project = \"{proj_path}\"") } else if let Some(grp_path) = group_path { format!( "type = Epic AND id = {work_item_id} AND group = \"{grp_path}\" AND includeSubgroups = false" ) } else { format!("id = {work_item_id} AND group = \"{parent_group}\"") } } else if let Some(grp_path) = group_path { format!( "type = Epic AND id = {work_item_id} AND group = \"{grp_path}\" AND includeSubgroups = false" ) } else { format!("id = {work_item_id} AND group = \"{parent_group}\"") }; subquery(query) } } #[cfg(test)] Loading src/parser/special.rs +65 −7 Original line number Diff line number Diff line use super::helpers::parse_reference; use crate::errors::{GlqlError, GlqlErrorKind::*}; use crate::types::{ ReferenceType, ReferenceType::*, Value::{self, *}, }; Loading Loading @@ -100,27 +101,60 @@ pub fn iteration(input: &str) -> IResult<&str, Value, GlqlError> { parse_reference(IterationRef, matcher, InvalidIteration, false)(input) } // Parser for epic references // Format: [group_path]&<epic_id> // Generic parser for work item references (epics and issues) // Format: [path]<separator><id> // Examples: // &123 - Epic with ID 123 in the current context // gitlab-org&123 - Epic with ID 123 in the gitlab-org group // #456 - Issue with ID 456 in the current context // gitlab-org/gitlab#456 - Issue with ID 456 in the gitlab-org/gitlab project // // Constraints: // - Path can only contain alphanumeric characters, hyphens, and forward slashes // - Path can only contain alphanumeric characters, hyphens, underscores, dots, and forward slashes // - ID must be numeric pub fn epic(input: &str) -> IResult<&str, Value, GlqlError> { fn work_item( input: &str, separator: char, ref_type: ReferenceType, ) -> IResult<&str, Value, GlqlError> { let (input, path) = take_while(|c: char| c.is_alphanumeric() || "/-_.".contains(c))(input)?; let (input, _) = tag("&")(input)?; let separator_str = separator.to_string(); let (input, _) = tag(separator_str.as_str())(input)?; let (input, id) = take_while1(|c: char| c.is_numeric())(input)?; let reference = if path.is_empty() { id.to_string() } else { format!("{path}&{id}") format!("{path}{separator}{id}") }; Ok((input, Reference(EpicRef, reference))) Ok((input, Reference(ref_type, reference))) } // Parser for epic references // Format: [group_path]&<epic_id> // Examples: // &123 - Epic with ID 123 in the current context // gitlab-org&123 - Epic with ID 123 in the gitlab-org group // // Constraints: // - Path can only contain alphanumeric characters, hyphens, and forward slashes // - ID must be numeric pub fn epic(input: &str) -> IResult<&str, Value, GlqlError> { work_item(input, '&', EpicRef) } // Parser for issue/work item references // Format: [project_path]#<issue_id> // Examples: // #123 - Issue with ID 123 in the current context // gitlab-org/gitlab#123 - Issue with ID 123 in the gitlab-org/gitlab project // // Constraints: // - Path can only contain alphanumeric characters, hyphens, underscores, dots, and forward slashes // - ID must be numeric pub fn issue(input: &str) -> IResult<&str, Value, GlqlError> { work_item(input, '#', WorkItemRef) } #[cfg(test)] Loading Loading @@ -282,4 +316,28 @@ mod tests { test_parser_error(epic, "path/to/project with spaces"); test_parser_error(epic, "path/to/project-without-id"); } #[test] fn test_issue() { test_parser_result( issue, "path/to/project-with_hyphens-underscores.and.dots#123", Reference( WorkItemRef, "path/to/project-with_hyphens-underscores.and.dots#123".to_string(), ), ); test_parser_result( issue, "gitlab-org/gitlab#123", Reference(WorkItemRef, "gitlab-org/gitlab#123".to_string()), ); test_parser_result(issue, "#123", Reference(WorkItemRef, "123".to_string())); test_parser_error(issue, ""); test_parser_error(issue, "#"); test_parser_error(issue, "#\"\""); test_parser_error(issue, "123"); test_parser_error(issue, "path/to/project with spaces"); test_parser_error(issue, "path/to/project-without-id"); } } src/parser/value.rs +2 −1 Original line number Diff line number Diff line use super::{ literals::{array, bool, function, null, number, string}, special::{date, epic, iteration, label, milestone, relative_date, token, username}, special::{date, epic, issue, iteration, label, milestone, relative_date, token, username}, }; use crate::errors::GlqlError; use crate::types::Value; Loading @@ -22,6 +22,7 @@ pub fn non_array_value(input: &str) -> IResult<&str, Value, GlqlError> { label, iteration, epic, issue, string, number, function, Loading Loading
src/analyzer/sources/issues.rs +3 −2 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ impl SourceAnalyzer for IssuesSourceAnalyzer { | Status | Label | Epic | Parent | Created | Updated | Assignee Loading Loading @@ -114,8 +115,8 @@ impl SourceAnalyzer for IssuesSourceAnalyzer { | EnumLike(vec!["current".into()]) | ReferenceLike(IterationRef) } Epic => { // Use the new field mapping abstraction for Epic field Epic | Parent => { // Use the new field mapping abstraction for Epic and Parent fields let context = Context { source: Some(crate::types::Source::Issues), ..Context::default() Loading
src/codegen/graphql_filters.rs +34 −2 Original line number Diff line number Diff line Loading @@ -85,7 +85,7 @@ impl GraphQLFilters { constraints.insert("excludeProjects".to_string(), Bool(true)); } if query.contains_field(&Epic) { if query.contains_field(&Epic) || query.contains_field(&Parent) { constraints.insert( match FeatureFlag::GlqlWorkItems.get() { true => "includeDescendantWorkItems".to_string(), Loading Loading @@ -217,7 +217,7 @@ fn to_graphql_filter_value(expr: &Expression, context: &mut Context) -> Value { (Assignee, Number(n)) => Quoted(n.to_string()), (Label | Approver, Token(s)) => Quoted(s.clone()), (Label | Assignee | Approver, Null) => Quoted("NONE".to_string()), (Weight | Epic, Null) => Token("NONE".to_string()), (Weight | Epic | Parent, Null) => Token("NONE".to_string()), (Weight, Number(n)) => Quoted(n.to_string()), (State, Token(s)) => Token(s.to_lowercase()), (Status, Quoted(s)) => Token(format!("{{name: \"{s}\"}}")), Loading Loading @@ -257,6 +257,27 @@ fn to_graphql_filter_value(expr: &Expression, context: &mut Context) -> Value { v.clone() } } (Parent, v) => { if let Some(mapping) = get_field_mapping(&Parent, context) { use crate::field_mapping::transform_epic_subquery_with_variable_type; let subquery = transform_epic_subquery_with_variable_type( &Parent, v, context, &mapping.variable_type, ); // graphql variables are not automatically coerced to lists, // so we need to wrap the subquery in a list, if it isn't already if matches!(subquery, Subquery(..)) { List(vec![subquery]) } else { subquery } } else { v.clone() } } (_, v) => v.clone(), } } Loading Loading @@ -335,6 +356,17 @@ fn to_graphql_filter_key(expr: &Expression, context: &Context) -> String { } } } (Parent, _, _) => { if let Some(mapping) = get_field_mapping(&Parent, context) { (mapping.get_attribute)(&mapping.field_type, &expr.value) } else { // Fallback match &expr.value { Null | Token(_) => "parentWildcardId".to_string(), _ => "parentIds".to_string(), } } } (Label, In, _) => pluralize_attribute("labelName").to_string(), (Label, _, _) => "labelName".to_string(), (Health, _, _) => "healthStatusFilter".to_string(), Loading
src/field_mapping.rs +86 −25 Original line number Diff line number Diff line Loading @@ -3,7 +3,6 @@ use crate::types::{ Context, Field, Field::*, FieldMapping, FieldType, FieldType::*, ReferenceType::*, RelationshipType::*, Source::*, Value, }; use crate::utils::common::parse_epic_reference; use crate::utils::feature_flags::FeatureFlag; /// Attribute function for work items epic mapping (parent work items) Loading @@ -19,6 +18,19 @@ fn epic_work_items_attribute(_field_type: &FieldType, value: &Value) -> String { } } /// Attribute function for parent work items mapping fn parent_work_items_attribute(_field_type: &FieldType, value: &Value) -> String { use crate::types::Value::*; // For parent field, wildcard values (null/tokens) use wildcard attribute // All other values (including lists) use parentIds if matches!(value, Null | Token(_)) { "parentWildcardId".to_string() } else { "parentIds".to_string() } } /// Attribute function for legacy epic mapping fn epic_legacy_attribute(field_type: &FieldType, value: &Value) -> String { use crate::types::Value::*; Loading Loading @@ -58,13 +70,25 @@ pub fn get_field_mapping(field: &Field, context: &Context) -> Option<FieldMappin get_attribute: epic_legacy_attribute, }), // Future: Parent field implementation would go here // (Field::Parent, true, Some(Issues)) => Some(FieldMapping { // field_type: StringLike | NumberLike | ReferenceLike(WorkItemRef) | // ListLike(HasOne, Box::new(StringLike | NumberLike | ReferenceLike(WorkItemRef))), // variable_type: "WorkItemID!".to_string(), // get_attribute: parent_work_items_attribute, // Different logic - no group derivation // }), // Parent field with work items API (Field::Parent, true, Some(Issues)) => Some(FieldMapping { field_type: StringLike | NumberLike | ReferenceLike(WorkItemRef) | ReferenceLike(EpicRef) | ListLike( HasOne, Box::new( StringLike | NumberLike | ReferenceLike(WorkItemRef) | ReferenceLike(EpicRef), ), ), variable_type: "WorkItemID!".to_string(), get_attribute: parent_work_items_attribute, }), _ => None, } } Loading @@ -72,40 +96,51 @@ pub fn get_field_mapping(field: &Field, context: &Context) -> Option<FieldMappin /// Transforms a value for work items epic references /// Handles epic references like "&123" or "group&123" and generates work items API subqueries pub fn transform_to_work_item_epic_subquery(v: &Value, context: &mut Context) -> Value { transform_epic_subquery_with_variable_type(v, context, "WorkItemID!") transform_epic_subquery_with_variable_type(&Epic, v, context, "WorkItemID!") } /// Transforms a value for legacy epic references /// Handles epic references for legacy API and generates legacy epics API subqueries pub fn transform_to_legacy_epic_subquery(v: &Value, context: &mut Context) -> Value { transform_epic_subquery_with_variable_type(v, context, "String") transform_epic_subquery_with_variable_type(&Epic, v, context, "String") } /// Common epic subquery transformation logic with configurable variable type fn transform_epic_subquery_with_variable_type( pub fn transform_epic_subquery_with_variable_type( field: &Field, v: &Value, context: &mut Context, variable_type: &str, ) -> Value { use crate::types::{Value::*, Variable}; use crate::utils::common::unique_id; use crate::utils::common::{parse_work_item_reference, unique_id}; use std::path::Path; let id = match v { Number(n) => n.to_string(), Quoted(s) => s.clone(), Reference(_, s) => s.clone(), Reference(ref_type, s) => { let id_str = s.clone(); // Add prefix if not present match ref_type { EpicRef if !id_str.contains('&') => format!("&{}", id_str), WorkItemRef if !id_str.contains('#') => format!("#{}", id_str), _ => id_str, } } List(arr) => { return List( arr.iter() .map(|v| transform_epic_subquery_with_variable_type(v, context, variable_type)) .map(|v| { transform_epic_subquery_with_variable_type(field, v, context, variable_type) }) .collect(), ); } _ => return v.clone(), }; let epic_group = match &context.group { let parent_group = match &context.group { Some(group) => group.clone(), None => { let project_path = context.project.clone().unwrap_or("".to_string()); Loading @@ -126,27 +161,53 @@ fn transform_epic_subquery_with_variable_type( ..Context::default() }; let variable_key = unique_id("epicId"); let default_query = |group: &str, id: &str| { format!("type = Epic AND id = {id} AND includeSubgroups = false AND group = \"{group}\"") }; let is_epic = matches!(field.dealias(), Epic); let variable_key = unique_id(if is_epic { "epicId" } else { "parentId" }); let subquery = |query| { let subquery = |query: String| { context.variables.push( Variable::new(&variable_key, variable_type) .with_value(&compile_glql(query, &mut subquery_context).output), .with_value(&compile_glql(&query, &mut subquery_context).output), ); Subquery( query.to_string(), query, Variable::new(&variable_key, variable_type), Box::new(subquery_context), ) }; // Parse the epic reference using the utility function let (group_path, epic_id) = parse_epic_reference(&id, &epic_group); subquery(&default_query(&group_path, &epic_id)) // Parse the work item reference using the utility function if is_epic { use crate::utils::common::parse_epic_reference; let (group_path, epic_id) = parse_epic_reference(&id, &parent_group); subquery(format!( "type = Epic AND id = {epic_id} AND includeSubgroups = false AND group = \"{group_path}\"" )) } else { // Parent field - can be either epic or work item reference let (group_path, project_path, work_item_id) = parse_work_item_reference(&id, &parent_group); let query = if let Some(proj_path) = project_path { if !proj_path.is_empty() { format!("id = {work_item_id} AND project = \"{proj_path}\"") } else if let Some(grp_path) = group_path { format!( "type = Epic AND id = {work_item_id} AND group = \"{grp_path}\" AND includeSubgroups = false" ) } else { format!("id = {work_item_id} AND group = \"{parent_group}\"") } } else if let Some(grp_path) = group_path { format!( "type = Epic AND id = {work_item_id} AND group = \"{grp_path}\" AND includeSubgroups = false" ) } else { format!("id = {work_item_id} AND group = \"{parent_group}\"") }; subquery(query) } } #[cfg(test)] Loading
src/parser/special.rs +65 −7 Original line number Diff line number Diff line use super::helpers::parse_reference; use crate::errors::{GlqlError, GlqlErrorKind::*}; use crate::types::{ ReferenceType, ReferenceType::*, Value::{self, *}, }; Loading Loading @@ -100,27 +101,60 @@ pub fn iteration(input: &str) -> IResult<&str, Value, GlqlError> { parse_reference(IterationRef, matcher, InvalidIteration, false)(input) } // Parser for epic references // Format: [group_path]&<epic_id> // Generic parser for work item references (epics and issues) // Format: [path]<separator><id> // Examples: // &123 - Epic with ID 123 in the current context // gitlab-org&123 - Epic with ID 123 in the gitlab-org group // #456 - Issue with ID 456 in the current context // gitlab-org/gitlab#456 - Issue with ID 456 in the gitlab-org/gitlab project // // Constraints: // - Path can only contain alphanumeric characters, hyphens, and forward slashes // - Path can only contain alphanumeric characters, hyphens, underscores, dots, and forward slashes // - ID must be numeric pub fn epic(input: &str) -> IResult<&str, Value, GlqlError> { fn work_item( input: &str, separator: char, ref_type: ReferenceType, ) -> IResult<&str, Value, GlqlError> { let (input, path) = take_while(|c: char| c.is_alphanumeric() || "/-_.".contains(c))(input)?; let (input, _) = tag("&")(input)?; let separator_str = separator.to_string(); let (input, _) = tag(separator_str.as_str())(input)?; let (input, id) = take_while1(|c: char| c.is_numeric())(input)?; let reference = if path.is_empty() { id.to_string() } else { format!("{path}&{id}") format!("{path}{separator}{id}") }; Ok((input, Reference(EpicRef, reference))) Ok((input, Reference(ref_type, reference))) } // Parser for epic references // Format: [group_path]&<epic_id> // Examples: // &123 - Epic with ID 123 in the current context // gitlab-org&123 - Epic with ID 123 in the gitlab-org group // // Constraints: // - Path can only contain alphanumeric characters, hyphens, and forward slashes // - ID must be numeric pub fn epic(input: &str) -> IResult<&str, Value, GlqlError> { work_item(input, '&', EpicRef) } // Parser for issue/work item references // Format: [project_path]#<issue_id> // Examples: // #123 - Issue with ID 123 in the current context // gitlab-org/gitlab#123 - Issue with ID 123 in the gitlab-org/gitlab project // // Constraints: // - Path can only contain alphanumeric characters, hyphens, underscores, dots, and forward slashes // - ID must be numeric pub fn issue(input: &str) -> IResult<&str, Value, GlqlError> { work_item(input, '#', WorkItemRef) } #[cfg(test)] Loading Loading @@ -282,4 +316,28 @@ mod tests { test_parser_error(epic, "path/to/project with spaces"); test_parser_error(epic, "path/to/project-without-id"); } #[test] fn test_issue() { test_parser_result( issue, "path/to/project-with_hyphens-underscores.and.dots#123", Reference( WorkItemRef, "path/to/project-with_hyphens-underscores.and.dots#123".to_string(), ), ); test_parser_result( issue, "gitlab-org/gitlab#123", Reference(WorkItemRef, "gitlab-org/gitlab#123".to_string()), ); test_parser_result(issue, "#123", Reference(WorkItemRef, "123".to_string())); test_parser_error(issue, ""); test_parser_error(issue, "#"); test_parser_error(issue, "#\"\""); test_parser_error(issue, "123"); test_parser_error(issue, "path/to/project with spaces"); test_parser_error(issue, "path/to/project-without-id"); } }
src/parser/value.rs +2 −1 Original line number Diff line number Diff line use super::{ literals::{array, bool, function, null, number, string}, special::{date, epic, iteration, label, milestone, relative_date, token, username}, special::{date, epic, issue, iteration, label, milestone, relative_date, token, username}, }; use crate::errors::GlqlError; use crate::types::Value; Loading @@ -22,6 +22,7 @@ pub fn non_array_value(input: &str) -> IResult<&str, Value, GlqlError> { label, iteration, epic, issue, string, number, function, Loading