Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parse parameter type annotations when generating native query configuration #128

Merged
merged 15 commits into from
Nov 22, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
parse parameter type annotations, and apply parsed type as type varia…
…ble constraint
hallettj committed Nov 22, 2024
commit 4fb26c9ebbb70f5e655019d75629cdea37e2a31f
12 changes: 7 additions & 5 deletions crates/cli/src/native_query/aggregation_expression.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::BTreeMap;

use itertools::Itertools as _;
use itertools::{Either, Itertools as _};
use mongodb::bson::{Bson, Document};
use mongodb_support::BsonScalarType;
use nonempty::NonEmpty;
@@ -127,7 +127,6 @@ fn infer_type_from_aggregation_expression_document(
}
}

// TODO: propagate expected type based on operator used
fn infer_type_from_operator_expression(
context: &mut PipelineTypeContext<'_>,
desired_object_type_name: &str,
@@ -340,10 +339,13 @@ pub fn infer_type_from_reference_shorthand(
let t = match reference {
Reference::NativeQueryVariable {
name,
type_annotation: _,
type_annotation,
} => {
// TODO: read type annotation ENG-1249
context.register_parameter(name.into(), type_hint.into_iter().cloned())
let constraints = match type_annotation {
Some(annotation) => Either::Left(std::iter::once(TypeConstraint::from(annotation))),
None => Either::Right(type_hint.into_iter().cloned()),
};
context.register_parameter(name.into(), constraints)
}
Reference::PipelineVariable { .. } => todo!("pipeline variable"),
Reference::InputDocumentField { name, nested_path } => {
3 changes: 3 additions & 0 deletions crates/cli/src/native_query/error.rs
Original file line number Diff line number Diff line change
@@ -78,6 +78,9 @@ pub enum Error {
#[error("Error parsing a string in the aggregation pipeline: {0}")]
UnableToParseReferenceShorthand(String),

#[error("Error parsing parameter type annotation: {0}")]
UnableToParseTypeAnnotation(String),

#[error("Type inference is not currently implemented for the query document operator, {0}. Please file a bug report, and declare types for your native query by hand for the time being.")]
UnknownMatchDocumentOperator(String),

1 change: 1 addition & 0 deletions crates/cli/src/native_query/mod.rs
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ mod pipeline;
mod pipeline_type_context;
mod prune_object_types;
mod reference_shorthand;
mod type_annotation;
mod type_constraint;
mod type_solver;

8 changes: 6 additions & 2 deletions crates/cli/src/native_query/pipeline/match_stage.rs
Original file line number Diff line number Diff line change
@@ -262,9 +262,13 @@ fn analyze_match_expression_string(
match parse_reference_shorthand(&match_expression)? {
Reference::NativeQueryVariable {
name,
type_annotation: _, // TODO: parse type annotation ENG-1249
type_annotation,
} => {
context.register_parameter(name.into(), [field_type.clone()]);
let constraints = match type_annotation {
Some(type_annotation) => [type_annotation.into()],
None => [field_type.clone()],
};
context.register_parameter(name.into(), constraints);
}
Reference::String {
native_query_variables,
12 changes: 8 additions & 4 deletions crates/cli/src/native_query/reference_shorthand.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use configuration::schema::Type;
use ndc_models::FieldName;
use nom::{
branch::alt,
@@ -9,15 +10,18 @@ use nom::{
IResult,
};

use super::error::{Error, Result};
use super::{
error::{Error, Result},
type_annotation::type_expression,
};

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Reference {
/// Reference to a variable that is substituted by the connector from GraphQL inputs before
/// sending to MongoDB. For example, `"{{ artist_id }}`.
NativeQueryVariable {
name: String,
type_annotation: Option<String>,
type_annotation: Option<Type>,
},

/// Reference to a variable that is defined as part of the pipeline syntax. May be followed by
@@ -66,7 +70,7 @@ fn native_query_variable(input: &str) -> IResult<&str, Reference> {
content.trim()
})(input)
};
let type_annotation = preceded(tag("|"), placeholder_content);
let type_annotation = preceded(tag("|"), type_expression);

let (remaining, (name, variable_type)) = delimited(
tag("{{"),
@@ -78,7 +82,7 @@ fn native_query_variable(input: &str) -> IResult<&str, Reference> {

let variable = Reference::NativeQueryVariable {
name: name.to_string(),
type_annotation: variable_type.map(ToString::to_string),
type_annotation: variable_type,
};
Ok((remaining, variable))
}
135 changes: 135 additions & 0 deletions crates/cli/src/native_query/type_annotation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use configuration::schema::Type;
use enum_iterator::all;
use mongodb_support::BsonScalarType;
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{alpha1, alphanumeric1},
combinator::{all_consuming, cut, opt, recognize},
error::ParseError,
multi::many0_count,
sequence::{delimited, pair, preceded},
IResult, Parser,
};

use super::error::{Error, Result};

/// Parse a type expression according to GraphQL syntax, using MongoDB scalar type names.
///
/// This implies that types are nullable by default unless they use the non-nullable suffix (!).
pub fn parse_type_annotation(input: &str) -> Result<Type> {
match type_expression(input) {
Ok((_, r)) => Ok(r),
Err(err) => Err(Error::UnableToParseTypeAnnotation(format!("{err}"))),
}
}

/// Nom parser for type expressions
pub fn type_expression(input: &str) -> IResult<&str, Type> {
all_consuming(nullability_suffix(alt((
extended_json_annotation,
scalar_annotation,
predicate_annotation,
object_annotation, // object_annotation must follow parsers that look for fixed sets of keywords
array_of_annotation,
))))(input)
}

fn extended_json_annotation(input: &str) -> IResult<&str, Type> {
let (remaining, _) = tag("extendedJSON")(input)?;
Ok((remaining, Type::ExtendedJSON))
}

fn scalar_annotation(input: &str) -> IResult<&str, Type> {
let scalar_type_parsers = all::<BsonScalarType>()
.map(|t| tag(t.bson_name()).map(move |_| Type::Nullable(Box::new(Type::Scalar(t)))));
all_consuming(alt_many(scalar_type_parsers))(input)
}

fn object_annotation(input: &str) -> IResult<&str, Type> {
let (remaining, name) = object_type_name(input)?;
Ok((
remaining,
Type::Nullable(Box::new(Type::Object(name.into()))),
))
}

fn predicate_annotation(input: &str) -> IResult<&str, Type> {
let (remaining, name) = preceded(
tag("predicate"),
delimited(tag("<"), cut(object_type_name), tag(">")),
)(input)?;
Ok((
remaining,
Type::Nullable(Box::new(Type::Predicate {
object_type_name: name.into(),
})),
))
}

fn object_type_name(input: &str) -> IResult<&str, &str> {
let first_char = alt((alpha1, tag("_")));
let succeeding_char = alt((alphanumeric1, tag("_")));
recognize(pair(first_char, many0_count(succeeding_char)))(input)
}

fn array_of_annotation(input: &str) -> IResult<&str, Type> {
delimited(tag("["), cut(type_expression), tag("]"))(input)
}

/// The other parsers produce nullable types by default. This wraps a parser that produces a type,
/// and flips the type from nullable to non-nullable if it sees the non-nullable suffix (!).
fn nullability_suffix<'a, P, E>(mut parser: P) -> impl FnMut(&'a str) -> IResult<&'a str, Type, E>
where
P: Parser<&'a str, Type, E> + 'a,
E: ParseError<&'a str>,
{
move |input| {
let (remaining, t) = parser.parse(input)?;
let (remaining, non_nullable_suffix) = opt(tag("!"))(remaining)?;
let t = match non_nullable_suffix {
None => t,
Some(_) => match t {
Type::Nullable(t) => *t,
t => t,
},
};
Ok((remaining, t))
}
}

/// Like [nom::branch::alt], but accepts a dynamically-constructed iterable of parsers instead of
/// a tuple.
///
/// From https://stackoverflow.com/a/76759023/103017
pub fn alt_many<I, O, E, P, Ps>(mut parsers: Ps) -> impl Parser<I, O, E>
where
P: Parser<I, O, E>,
I: Clone,
for<'a> &'a mut Ps: IntoIterator<Item = P>,
E: ParseError<I>,
{
move |input: I| {
for mut parser in &mut parsers {
if let r @ Ok(_) = parser.parse(input.clone()) {
return r;
}
}
nom::combinator::fail::<I, O, E>(input)
}
}

#[cfg(test)]
mod tests {
use proptest::{prop_assert_eq, proptest};
use test_helpers::arb_type;

proptest! {
#[test]
fn test_parse_type_annotation(t in arb_type()) {
let annotation = t.to_string();
let parsed = super::parse_type_annotation(&annotation);
prop_assert_eq!(parsed, t)
}
}
}