Skip to content

Commit

Permalink
Merge pull request #7 from danielgtaylor/fix-empty-array-unquoted-typ…
Browse files Browse the repository at this point in the history
…echeck

fix: empty array bug and unquoted typechecking
  • Loading branch information
danielgtaylor authored Dec 12, 2022
2 parents 386f601 + 5d7f2d0 commit 2fc17ca
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 9 deletions.
8 changes: 4 additions & 4 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ package mexpr
// Parse an expression and return the abstract syntax tree. If `types` is
// passed, it should be a set of representative example values for the input
// which will be used to type check the expression against.
func Parse(expression string, types map[string]interface{}) (*Node, Error) {
func Parse(expression string, types map[string]interface{}, options ...InterpreterOption) (*Node, Error) {
l := NewLexer(expression)
p := NewParser(l)
ast, err := p.Parse()
if err != nil {
return nil, err
}
if types != nil {
if err := TypeCheck(ast, types); err != nil {
if err := TypeCheck(ast, types, options...); err != nil {
return nil, err
}
}
Expand All @@ -21,8 +21,8 @@ func Parse(expression string, types map[string]interface{}) (*Node, Error) {

// TypeCheck will take a parsed AST and type check against the given input
// structure with representative example values.
func TypeCheck(ast *Node, types map[string]interface{}) Error {
i := NewTypeChecker(ast)
func TypeCheck(ast *Node, types map[string]interface{}, options ...InterpreterOption) Error {
i := NewTypeChecker(ast, options...)
return i.Run(types)
}

Expand Down
3 changes: 3 additions & 0 deletions interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,9 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) {
return nil, err
}
results := []any{}
if resultLeft == nil {
return nil, nil
}
for _, item := range resultLeft.([]any) {
resultRight, _ := i.run(ast.Right, item)
if i.strict && err != nil {
Expand Down
4 changes: 3 additions & 1 deletion interpreter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func TestInterpreter(t *testing.T) {
{expr: `@.foo + 1`, opts: []InterpreterOption{UnquotedStrings, StrictMode}, err: "cannot get foo"},
{expr: `foo.bar == bar`, opts: []InterpreterOption{UnquotedStrings}, output: false},
{expr: `foo.bar == bar`, skipTC: true, opts: []InterpreterOption{UnquotedStrings}, input: `{"foo": {}}`, output: false},
{expr: `foo.bar == baz`, opts: []InterpreterOption{UnquotedStrings}, input: `{"foo": {"bar": "baz"}}`, output: true},
// Identifier / fields
{expr: "foo", input: `{"foo": 1.0}`, output: 1.0},
{expr: "foo.bar.baz", input: `{"foo": {"bar": {"baz": 1.0}}}`, output: 1.0},
Expand Down Expand Up @@ -137,6 +138,7 @@ func TestInterpreter(t *testing.T) {
{expr: `items where id > 3 where labels contains "foo"`, input: `{"items": [{"id": 1, "labels": ["foo"]}, {"id": 3}, {"id": 5, "labels": ["foo"]}, {"id": 7}]}`, output: []interface{}{map[string]interface{}{"id": 5.0, "labels": []interface{}{"foo"}}}},
{expr: `(items where id > 3).length == 2`, input: `{"items": [{"id": 1}, {"id": 3}, {"id": 5}, {"id": 7}]}`, output: true},
{expr: `not (items where id > 3)`, input: `{"items": [{"id": 1}, {"id": 3}, {"id": 5}, {"id": 7}]}`, output: false},
{expr: `items where id > 3`, input: `{}`, skipTC: true, output: nil},
// Order of operations
{expr: "1 + 2 + 3", output: 6.0},
{expr: "1 + 2 * 3", output: 7.0},
Expand Down Expand Up @@ -181,7 +183,7 @@ func TestInterpreter(t *testing.T) {
// Skip type check
types = nil
}
ast, err := Parse(tc.expr, types)
ast, err := Parse(tc.expr, types, tc.opts...)

if tc.err != "" {
if err != nil {
Expand Down
34 changes: 30 additions & 4 deletions typecheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,26 @@ type TypeChecker interface {
}

// NewTypeChecker returns a type checker for the given AST.
func NewTypeChecker(ast *Node) TypeChecker {
func NewTypeChecker(ast *Node, options ...InterpreterOption) TypeChecker {
unquoted := false

for _, opt := range options {
switch opt {
case UnquotedStrings:
unquoted = true
}
}

return &typeChecker{
ast: ast,
ast: ast,
unquoted: unquoted,
}
}

type typeChecker struct {
ast *Node
ast *Node
prevFieldSelect bool
unquoted bool
}

func (i *typeChecker) Run(value any) Error {
Expand All @@ -113,6 +125,9 @@ func (i *typeChecker) runBoth(ast *Node, value any) (*schema, *schema, Error) {
}

func (i *typeChecker) run(ast *Node, value any) (*schema, Error) {
fromSelect := i.prevFieldSelect
i.prevFieldSelect = false

switch ast.Type {
case NodeIdentifier:
switch ast.Value.(string) {
Expand All @@ -126,12 +141,17 @@ func (i *typeChecker) run(ast *Node, value any) (*schema, Error) {
case "lower", "upper":
return schemaString, nil
}
errValue := value
if s, ok := value.(*schema); ok {
if v, ok := s.properties[ast.Value.(string)]; ok {
return v, nil
}
keys := []string{}
for k := range s.properties {
keys = append(keys, k)
}
errValue = "map with keys [" + strings.Join(keys, ", ") + "]"
}
errValue := value
if m, ok := value.(map[string]any); ok {
if v, ok := m[ast.Value.(string)]; ok {
return getSchema(v), nil
Expand All @@ -152,12 +172,18 @@ func (i *typeChecker) run(ast *Node, value any) (*schema, Error) {
}
errValue = "map with keys [" + strings.Join(keys, ", ") + "]"
}
if i.unquoted && !fromSelect {
// Identifiers not found in the map are treated as strings, but only if
// the previous item was not a `.` like `obj.field`.
return schemaString, nil
}
return nil, NewError(ast.Offset, ast.Length, "no property %v in %v", ast.Value, errValue)
case NodeFieldSelect:
leftType, err := i.run(ast.Left, value)
if err != nil {
return nil, err
}
i.prevFieldSelect = true
return i.run(ast.Right, leftType)
case NodeArrayIndex:
leftType, rightType, err := i.runBoth(ast, value)
Expand Down

0 comments on commit 2fc17ca

Please sign in to comment.