Skip to content

Commit

Permalink
feat: add parser
Browse files Browse the repository at this point in the history
  • Loading branch information
jimlambrt committed Aug 7, 2023
1 parent 3f24869 commit f6827ef
Show file tree
Hide file tree
Showing 6 changed files with 641 additions and 0 deletions.
12 changes: 12 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,17 @@ package mql
import "errors"

var (
ErrInvalidParameter = errors.New("invalid parameter")
ErrInvalidNotEqual = errors.New(`invalid "!=" token`)
ErrMissingLogicalExpr = errors.New("missing logical expression")
ErrUnexpectedExpr = errors.New("unexpected expression")
ErrUnexpectedClosingParen = errors.New("unexpected closing paren")
ErrMissingClosingParen = errors.New("missing closing paren")
ErrUnexpectedLogicalOp = errors.New("unexpected logical operator")
ErrUnexpectedToken = errors.New("unexpected token")
ErrInvalidComparisonOp = errors.New("invalid comparison operator")
ErrMissingComparisonOp = errors.New("missing comparison operator")
ErrInvalidLogicalOp = errors.New("invalid logical operator")
ErrMissingLogicalOp = errors.New("missing logical operator")
ErrMissingRightSideExpr = errors.New("logical operator without a right side expr")
)
116 changes: 116 additions & 0 deletions expr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) HashiCorp, Inc.

package mql

import (
"fmt"
)

type exprType int

const (
comparisonExprType exprType = iota
logicalExprType
)

var exprTypeToString = map[exprType]string{
comparisonExprType: "Comparison Expression",
logicalExprType: "Logical Expression",
}

type expr interface {
Type() exprType
}

type comparisonOp string

const (
greaterThanOp comparisonOp = ">"
greaterThanOrEqualOp = ">="
lessThanOp = "<"
lessThanOrEqualOp = "<="
equalOp = "="
notEqualOp = "!="
containsOp = "%"
)

func newComparisonOp(s string) (comparisonOp, error) {
const op = "newComparisonOp"
switch s {
case
string(greaterThanOp),
string(greaterThanOrEqualOp),
string(lessThanOp),
string(lessThanOrEqualOp),
string(equalOp),
string(notEqualOp),
string(containsOp):
return comparisonOp(s), nil
default:
return "", fmt.Errorf("%s: %w %q", op, ErrInvalidComparisonOp, s)
}
}

type comparisonExpr struct {
column string
comparisonOp comparisonOp
value *string
}

func (e *comparisonExpr) Type() exprType {
return comparisonExprType
}

type logicalOp string

const (
andOp logicalOp = "and"
orOp = "or"
)

func newLogicalOp(s string) (logicalOp, error) {
const op = "newLogicalOp"
switch s {
case
string(andOp),
string(orOp):
return logicalOp(s), nil
default:
return "", fmt.Errorf("%s: %w %q", op, ErrInvalidLogicalOp, s)
}
}

type logicalExpr struct {
leftExpr expr
logicalOp logicalOp
rightExpr expr
}

func (c *logicalExpr) Type() exprType {
return logicalExprType
}

// root will return the root of the expr tree
func root(lExpr *logicalExpr, raw string) (expr, error) {
const op = "mql.root"
switch {
case lExpr == nil:
return nil, fmt.Errorf("%s: %w (missing expression)", op, ErrInvalidParameter)
}
logicalOp := lExpr.logicalOp
if logicalOp != "" && lExpr.rightExpr == nil {
return nil, fmt.Errorf("%s: %w in: %q", op, ErrMissingRightSideExpr, raw)
}

for lExpr.logicalOp == "" {
switch {
case lExpr.leftExpr == nil:
return nil, fmt.Errorf("%s: %w nil", op, ErrMissingLogicalExpr)
case lExpr.leftExpr.Type() == comparisonExprType:
return lExpr.leftExpr, nil
default:
lExpr = lExpr.leftExpr.(*logicalExpr)
}
}
return lExpr, nil
}
34 changes: 34 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) HashiCorp, Inc.

package mql

type options struct {
witSkipWhitespace bool
}

// Option - how options are passed as args
type Option func(*options) error

func getDefaultOptions() options {
return options{}
}

func getOpts(opt ...Option) (options, error) {
opts := getDefaultOptions()

for _, o := range opt {
if err := o(&opts); err != nil {
return opts, err
}
}
return opts, nil
}

// withSkipWhitespace provides an option to request that whitespace be skipped
func withSkipWhitespace() Option {
const op = "mql.WithSkipWhitespace"
return func(o *options) error {
o.witSkipWhitespace = true
return nil
}
}
204 changes: 204 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Copyright (c) HashiCorp, Inc.

package mql

import (
"fmt"
)

type parser struct {
l *lexer
raw string
currentToken token
openLogicalExpr stack[struct{}] // something very simple to make sure every logical expr that's opened is closed.
}

func newParser(s string) *parser {
return &parser{
l: newLexer(s),
raw: s,
}
}

func (p *parser) parse() (expr, error) {
const op = "mdl.(parser).parse"
lExpr, err := p.parseLogicalExpr()
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
r, err := root(lExpr, p.raw)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
return r, nil
}

// parseLogicalExpr will parse a logicalExpr until an eofToken is reached, which
// may require it to parse a comparisonExpr and/or recursively parse
// logicalExprs
func (p *parser) parseLogicalExpr() (*logicalExpr, error) {
const op = "parseLogicalExprState"
logicExpr := &logicalExpr{}

if err := p.scan(withSkipWhitespace()); err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
TKLOOP:
for p.currentToken.Type != eofToken {
switch p.currentToken.Type {
case startLogicalExprToken: // there's a opening paren: (
// so we've found a new logical expr to parse
e, err := p.parseLogicalExpr()
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
switch {
// start by assigning the left expr
case logicExpr.leftExpr == nil:
logicExpr.leftExpr = e
break TKLOOP
// we should have a logical operator before the right side expr is assigned
case logicExpr.logicalOp == "":
return nil, fmt.Errorf("%s: %w before right side expression in: %q", op, ErrMissingLogicalOp, p.raw)
// finally, assign the right expr
case logicExpr.rightExpr == nil:
logicExpr.rightExpr = e.leftExpr
break TKLOOP
}
case stringToken:
if (logicExpr.leftExpr != nil && logicExpr.logicalOp == "") ||
(logicExpr.leftExpr != nil && logicExpr.rightExpr != nil) {
return nil, fmt.Errorf("%s: %w starting at %q in: %q", op, ErrUnexpectedExpr, p.currentToken.Value, p.raw)
}
cmpExpr, err := p.parseComparisonExpr()
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
switch {
case logicExpr.leftExpr == nil:
logicExpr.leftExpr = cmpExpr
case logicExpr.rightExpr == nil:
logicExpr.rightExpr = cmpExpr
tmpExpr := &logicalExpr{
leftExpr: logicExpr,
logicalOp: "",
rightExpr: nil,
}
logicExpr = tmpExpr
default:
return nil, fmt.Errorf("%s: %w at %q, but both left and right expressions already exist in: %q", op, ErrUnexpectedExpr, p.currentToken.Value, p.raw)
}
case endLogicalExprToken:
if logicExpr.leftExpr == nil {
return nil, fmt.Errorf("%s: %w %q but we haven't parsed a left side expression in: %q", op, ErrUnexpectedClosingParen, p.currentToken.Value, p.raw)
}
return logicExpr, nil
case andToken, orToken:
if logicExpr.logicalOp != "" {
return nil, fmt.Errorf("%s: %w %q when we've already parsed one for expr in: %q", op, ErrUnexpectedLogicalOp, p.currentToken.Value, p.raw)
}
o, err := newLogicalOp(p.currentToken.Value)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
logicExpr.logicalOp = o
default:
return nil, fmt.Errorf("%s: %w %q in: %q", op, ErrUnexpectedToken, p.currentToken.Value, p.raw)
}
if err := p.scan(withSkipWhitespace()); err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
}
if p.openLogicalExpr.len() > 0 {
return nil, fmt.Errorf("%s: %w in: %q", op, ErrMissingClosingParen, p.raw)
}
return logicExpr, nil
}

// parseComparisonExpr will parse a comparisonExpr until an eofToken is reached,
// which may require it to parse logicalExpr
func (p *parser) parseComparisonExpr() (expr, error) {
const op = "mql.(parser).parseComparisonExpr"
cmpExpr := &comparisonExpr{}

// our language (and this parser) def requires the tokens to be in the
// correct order: column, comparisonOp, value. Swapping this order where the
// value comes first (value, comparisonOp, column) is not supported
for p.currentToken.Type != eofToken {
switch {
case p.currentToken.Type == startLogicalExprToken:
return p.parseLogicalExpr()

// we found whitespace, so check if there's a completed logical expr to return
case p.currentToken.Type == whitespaceToken:
if cmpExpr.column != "" && cmpExpr.comparisonOp != "" && cmpExpr.value != nil {
return cmpExpr, nil
}

// columns must come first, so handle those conditions
case cmpExpr.column == "" && p.currentToken.Type != stringToken:
return nil, fmt.Errorf("")
case cmpExpr.column == "": // has to be stringToken representing the column
cmpExpr.column = p.currentToken.Value

// after columns, comparison operators must come next
case cmpExpr.comparisonOp == "":
c, err := newComparisonOp(p.currentToken.Value)
if err != nil {
return nil, fmt.Errorf("%s: %w %q in: %q", op, err, p.currentToken.Value, p.raw)
}
cmpExpr.comparisonOp = c

// finally, values must come at the end
case cmpExpr.value == nil && p.currentToken.Type != stringToken:
return nil, fmt.Errorf("%s: %w %q in: %q", op, ErrUnexpectedToken, p.currentToken.Value, p.raw)
case cmpExpr.value == nil:
s := p.currentToken.Value
cmpExpr.value = &s
}
if err := p.scan(); err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
}

switch {
case cmpExpr.column != "" && cmpExpr.comparisonOp == "":
return nil, fmt.Errorf("%s: %w in: %q", op, ErrMissingComparisonOp, p.raw)
default:
return cmpExpr, nil
}
}

// scan will get the next token from the lexer. Supported options:
// withSkipWhitespace
func (p *parser) scan(opt ...Option) error {
const op = "mql.(parser).scan"

opts, err := getOpts(opt...)
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}

if p.currentToken, err = p.l.nextToken(); err != nil {
return fmt.Errorf("%s: %w", op, err)
}

if opts.witSkipWhitespace {
for p.currentToken.Type == whitespaceToken {
if p.currentToken, err = p.l.nextToken(); err != nil {
return fmt.Errorf("%s: %w", op, err)
}
}
}

switch p.currentToken.Type {
case errToken:
return fmt.Errorf("%s: %s", op, p.currentToken.Value)
case startLogicalExprToken:
p.openLogicalExpr.push(struct{}{})
case endLogicalExprToken:
p.openLogicalExpr.pop()
}

return nil
}
Loading

0 comments on commit f6827ef

Please sign in to comment.