Skip to content

Commit

Permalink
Add lexer & parser support for conditional mutation (@if)
Browse files Browse the repository at this point in the history
  • Loading branch information
mangalaman93 committed Jun 28, 2019
1 parent d7376d7 commit b5b5754
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 17 deletions.
64 changes: 57 additions & 7 deletions gql/parser_mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ func ParseMutation(mutation string) (mu *api.Mutation, err error) {
item := it.Item()
switch item.Typ {
case itemUpsertBlock:
if mu, err = ParseUpsertBlock(it); err != nil {
if mu, err = parseUpsertBlock(it); err != nil {
return nil, err
}
case itemLeftCurl:
if mu, err = ParseMutationBlock(it); err != nil {
if mu, err = parseMutationBlock(it); err != nil {
return nil, err
}
default:
Expand All @@ -58,8 +58,8 @@ func ParseMutation(mutation string) (mu *api.Mutation, err error) {
return mu, nil
}

// ParseUpsertBlock parses the upsert block
func ParseUpsertBlock(it *lex.ItemIterator) (*api.Mutation, error) {
// parseUpsertBlock parses the upsert block
func parseUpsertBlock(it *lex.ItemIterator) (*api.Mutation, error) {
var mu *api.Mutation
var queryText string
var queryFound bool
Expand Down Expand Up @@ -109,8 +109,22 @@ func ParseUpsertBlock(it *lex.ItemIterator) (*api.Mutation, error) {
if !it.Next() {
return nil, errors.Errorf("Unexpected end of upsert block")
}

// upsert ===>@<===if(...) {....}
if it.Item().Typ == itemAt {
if err := parseIfDirective(it); err != nil {
return nil, err
}

// upsert @if(...===>)<=== {....}
if !it.Next() {
return nil, errors.Errorf("Unexpected end of upsert block")
}
}

// upsert @if(...) ===>{<=== ....}
var err error
if mu, err = ParseMutationBlock(it); err != nil {
if mu, err = parseMutationBlock(it); err != nil {
return nil, err
}

Expand All @@ -133,8 +147,44 @@ func ParseUpsertBlock(it *lex.ItemIterator) (*api.Mutation, error) {
return nil, errors.Errorf("Invalid upsert block")
}

// ParseMutationBlock parses the mutation block
func ParseMutationBlock(it *lex.ItemIterator) (*api.Mutation, error) {
// parseIfDirective parses the @if directive in mutation
func parseIfDirective(it *lex.ItemIterator) error {
if !it.Next() {
return errors.Errorf("Unexpected end of @if directive in upsert block")
}

// upsert @===>if<===(...) {....}
item := it.Item()
if item.Typ != itemName || item.Val != "if" {
return errors.Errorf("Expected [if], Got: [%s]", item.Val)
}

// TODO(Aman): Construct Tree/Graph to evaluate if condition.
depth := 0
for it.Next() {
item = it.Item()
switch item.Typ {
case itemLeftRound:
depth++
case itemRightRound:
depth--
if depth == 0 {
return nil
}
case itemName:
continue
case itemComma:
continue
default:
return errors.Errorf("Unexpected token in @if directive [%s]", item.Val)
}
}

return errors.Errorf("Invalid @if directive in upsert block")
}

// parseMutationBlock parses the mutation block
func parseMutationBlock(it *lex.ItemIterator) (*api.Mutation, error) {
var mu api.Mutation

item := it.Item()
Expand Down
45 changes: 35 additions & 10 deletions gql/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,7 @@ func lexIdentifyBlock(l *lex.Lexer) lex.StateFn {
func lexNameBlock(l *lex.Lexer) lex.StateFn {
for {
// The caller already checked isNameBegin, and absorbed one rune.
r := l.Next()
if isNameSuffix(r) {
continue
}
l.Backup()
l.AcceptRun(isNameSuffix)
switch word := l.Input[l.Start:l.Pos]; word {
case "upsert":
l.Emit(itemUpsertBlock)
Expand Down Expand Up @@ -140,11 +136,7 @@ func lexUpsertBlock(l *lex.Lexer) lex.StateFn {
func lexNameUpsertOp(l *lex.Lexer) lex.StateFn {
for {
// The caller already checked isNameBegin, and absorbed one rune.
r := l.Next()
if isNameSuffix(r) {
continue
}
l.Backup()
l.AcceptRun(isNameSuffix)
word := l.Input[l.Start:l.Pos]
switch word {
case "query":
Expand Down Expand Up @@ -187,10 +179,39 @@ func lexBlockContent(l *lex.Lexer) lex.StateFn {
}
}

// lexIfDirective lexes the @if directive in a mutation block
func lexIfDirective(l *lex.Lexer) lex.StateFn {
l.Mode = lexIfDirective
for {
switch r := l.Next(); {
case r == lex.EOF:
return l.Errorf("invalid if condition")
case isSpace(r) || lex.IsEndOfLine(r):
l.Ignore()
case r == '#':
return lexComment
case r == leftRound:
l.Emit(itemLeftRound)
l.AcceptRun(isSpace)
l.Ignore()
l.ArgDepth++
return lexFuncOrArg
case r == at:
l.Emit(itemAt)
return lexDirectiveOrLangList
default:
return l.Errorf("Unrecognized character in lexText: %#U", r)
}
}
}

func lexInsideMutation(l *lex.Lexer) lex.StateFn {
l.Mode = lexInsideMutation
for {
switch r := l.Next(); {
case r == at:
l.Backup()
return lexIfDirective
case r == rightCurl:
l.Depth--
l.Emit(itemRightCurl)
Expand Down Expand Up @@ -394,6 +415,10 @@ Loop:

// lexQuery lexes the input string and calls other lex functions.
func lexQuery(l *lex.Lexer) lex.StateFn {
if l.BlockDepth != 0 {
return lexInsideMutation
}

l.Mode = lexQuery
for {
switch r := l.Next(); {
Expand Down
122 changes: 122 additions & 0 deletions gql/upsert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,125 @@ func TestUpsertWithFilter(t *testing.T) {
_, err := ParseMutation(query)
require.Nil(t, err)
}

func TestConditionalUpsertWithNewlines(t *testing.T) {
query := `upsert {
mutation @if(eq(len(m), 1)
AND
gt(len(f), 0)) {
set {
uid(m) <age> "45" .
uid(f) <age> "45" .
}
}
query {
me(func: eq(age, 34)) @filter(ge(name, "user")) {
uid
friend {
uid
age
}
}
}
}
`
_, err := ParseMutation(query)
require.Nil(t, err)
}

func TestConditionalUpsertFuncTree(t *testing.T) {
query := `upsert {
mutation @if( ( eq(len(m), 1)
OR
lt(len(h), 90))
AND
gt(len(f), 0)) {
set {
uid(m) <age> "45" .
uid(f) <age> "45" .
}
}
query {
me(func: eq(age, 34)) @filter(ge(name, "user")) {
uid
friend {
uid
age
}
}
}
}
`
_, err := ParseMutation(query)
require.Nil(t, err)
}

func TestConditionalUpsertErrMissingRightRound(t *testing.T) {
query := `upsert {
mutation @if(eq(len(m, 1)
AND
gt(len(f), 0)) {
set {
uid(m) <age> "45" .
uid(f) <age> "45" .
}
}
query {
me(func: eq(age, 34)) @filter(ge(name, "user")) {
uid
friend {
uid
age
}
}
}
}
`
_, err := ParseMutation(query)
require.Contains(t, err.Error(), "Unrecognized character inside a func")
}

func TestConditionalUpsertErrUnclosed(t *testing.T) {
query := `upsert {
mutation @if(eq(len(m), 1) AND gt(len(f), 0))`
_, err := ParseMutation(query)
require.Contains(t, err.Error(), "Unclosed mutation action")
}

func TestConditionalUpsertErrInvalidIf(t *testing.T) {
query := `upsert {
mutation @if`
_, err := ParseMutation(query)
require.Contains(t, err.Error(), "invalid if condition")
}

func TestConditionalUpsertErrWrongIf(t *testing.T) {
query := `upsert {
mutation @fi( ( eq(len(m), 1)
OR
lt(len(h), 90))
AND
gt(len(f), 0)) {
set {
uid(m) <age> "45" .
uid(f) <age> "45" .
}
}
query {
me(func: eq(age, 34)) @filter(ge(name, "user")) {
uid
friend {
uid
age
}
}
}
}
`
_, err := ParseMutation(query)
require.Contains(t, err.Error(), "Expected [if], Got: [fi]")
}

0 comments on commit b5b5754

Please sign in to comment.