Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ jobs:
runs-on: ubuntu-latest
name: Build & Test
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v5
- name: Setup go
uses: actions/setup-go@v1
uses: actions/setup-go@v6
with:
go-version: "1.18"
go-version-file: go.mod
- run: go test -coverprofile=coverage.txt -covermode=atomic ./...
- uses: codecov/codecov-action@v1
- uses: codecov/codecov-action@v5
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Try it out on the [Go Playground](https://play.golang.org/p/Z0UcEBgfxu_r)! You c
import "github.com/danielgtaylor/mexpr"

// Convenience for lexing/parsing/running in one step:
result, err := mexpr.Eval("a > b", map[string]interface{}{
result, err := mexpr.Eval("a > b", map[string]any{
"a": 2,
"b": 1,
})
Comment thread
danielgtaylor marked this conversation as resolved.
Expand All @@ -42,18 +42,18 @@ result, err := mexpr.Eval("a > b", map[string]interface{}{
// omitted for brevity.
l := mexpr.NewLexer("a > b")
p := mexpr.NewParser(l)
ast, err := mexpr.Parse()
typeExamples = map[string]interface{}{
ast, err := p.Parse()
typeExamples := map[string]any{
"a": 2,
"b": 1,
}
err := mexpr.TypeCheck(ast, typeExamples)
Comment thread
danielgtaylor marked this conversation as resolved.
Outdated
interpreter := mexpr.NewInterpreter(ast)
result1, err := interpreter.Run(map[string]interface{}{
result1, err := interpreter.Run(map[string]any{
"a": 1,
"b": 2,
})
result2, err := interpreter.Run(map[string]interfae{}{
result2, err := interpreter.Run(map[string]any{
"a": 150,
"b": 30,
})
Expand All @@ -80,10 +80,11 @@ When running the interpreter a set of options can be passed in to change behavio

```go
// Using the top-level eval
mexpr.Eval(expression, inputObj, StrictMode)
result, err := mexpr.Eval(expression, inputObj, mexpr.StrictMode)

// Using an interpreter instance
interpreter.Run(inputObj, StrictMode)
interpreter := mexpr.NewInterpreter(ast, mexpr.StrictMode)
result, err = interpreter.Run(inputObj)
```

## Syntax
Expand Down Expand Up @@ -189,7 +190,7 @@ Indexes are zero-based. Slice indexes are optional and are _inclusive_. `foo[1:2

Any value concatenated with a string will result in a string. For example `"id" + 1` will result in `"id1"`.

There is no distinction between strings, bytes, or runes. Everything is treated as a string.
String length, indexing, and slicing are Unicode-aware and operate on runes rather than raw bytes.

#### Date Comparisons

Expand Down
35 changes: 24 additions & 11 deletions conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"fmt"
"reflect"
"time"
"unicode/utf8"
)

func isNumber(v interface{}) bool {
func isNumber(v any) bool {
switch v.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return true
Expand All @@ -22,7 +23,7 @@ func isNumber(v interface{}) bool {
return false
}

func toNumber(ast *Node, v interface{}) (float64, Error) {
func toNumber(ast *Node, v any) (float64, Error) {
switch n := v.(type) {
case float64:
return n, nil
Expand Down Expand Up @@ -76,7 +77,7 @@ func toNumber(ast *Node, v interface{}) (float64, Error) {
return 0, NewError(ast.Offset, ast.Length, "unable to convert to number: %v", v)
}

func isString(v interface{}) bool {
func isString(v any) bool {
switch v.(type) {
case string, rune, byte, []byte:
return true
Expand All @@ -86,7 +87,7 @@ func isString(v interface{}) bool {
return false
}

func toString(v interface{}) string {
func toString(v any) string {
switch s := v.(type) {
case string:
return s
Expand All @@ -102,9 +103,21 @@ func toString(v interface{}) string {
return fmt.Sprintf("%v", v)
}

func stringLength(v string) int {
return utf8.RuneCountInString(v)
}

func stringIndex(v string, idx int) string {
return string([]rune(v)[idx])
}

func stringSlice(v string, start, end int) string {
return string([]rune(v)[start : end+1])
Comment thread
danielgtaylor marked this conversation as resolved.
Outdated
}

// toTime converts a string value into a time.Time if possible, otherwise
// returns a zero time.
func toTime(v interface{}) time.Time {
func toTime(v any) time.Time {
vStr := toString(v)
if t, err := time.Parse(time.RFC3339, vStr); err == nil {
return t
Expand All @@ -118,14 +131,14 @@ func toTime(v interface{}) time.Time {
return time.Time{}
}

func isSlice(v interface{}) bool {
if _, ok := v.([]interface{}); ok {
func isSlice(v any) bool {
if _, ok := v.([]any); ok {
return true
}
return false
}

func toBool(v interface{}) bool {
func toBool(v any) bool {
switch n := v.(type) {
case bool:
return n
Expand Down Expand Up @@ -157,9 +170,9 @@ func toBool(v interface{}) bool {
return len(n) > 0
case []byte:
return len(n) > 0
case []interface{}:
case []any:
return len(n) > 0
case map[string]interface{}:
case map[string]any:
return len(n) > 0
case map[any]any:
return len(n) > 0
Expand All @@ -170,7 +183,7 @@ func toBool(v interface{}) bool {
// normalize an input for equality checks. All numbers -> float64, []byte to
// string, etc. Since `rune` is an alias for int32, we can't differentiate it
// for comparison with strings.
func normalize(v interface{}) interface{} {
func normalize(v any) any {
switch n := v.(type) {
case int:
return float64(n)
Expand Down
2 changes: 1 addition & 1 deletion error.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (e *exprErr) Pretty(source string) string {
}

// NewError creates a new error at a specific location.
func NewError(offset uint16, length uint8, format string, a ...interface{}) Error {
func NewError(offset uint16, length uint8, format string, a ...any) Error {
return &exprErr{
offset: offset,
length: length,
Expand Down
74 changes: 32 additions & 42 deletions interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ func checkBounds(ast *Node, input any, idx int) Error {
}
}
if v, ok := input.(string); ok {
if idx < 0 || idx >= len(v) {
return NewError(ast.Offset, ast.Length, "invalid index %d for string of length %d", int(idx), len(v))
if idx < 0 || idx >= stringLength(v) {
return NewError(ast.Offset, ast.Length, "invalid index %d for string of length %d", int(idx), stringLength(v))
Comment thread
danielgtaylor marked this conversation as resolved.
Outdated
}
}
return nil
Expand Down Expand Up @@ -101,10 +101,10 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) {
case "length":
// Special pseudo-property to get the value's length.
if s, ok := value.(func() string); ok {
return len(s()), nil
return stringLength(s()), nil
}
if s, ok := value.(string); ok {
return len(s), nil
return stringLength(s), nil
}
if a, ok := value.([]any); ok {
return len(a), nil
Expand Down Expand Up @@ -197,11 +197,12 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) {
return left[int(start) : int(end)+1], nil
}
left := toString(resultLeft)
leftLen := stringLength(left)
if start < 0 {
start += float64(len(left))
start += float64(leftLen)
}
if end < 0 {
end += float64(len(left))
end += float64(leftLen)
}
if err := checkBounds(ast, left, int(start)); err != nil {
return nil, err
Expand All @@ -212,7 +213,7 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) {
if err := checkBounds(ast, left, int(end)); err != nil {
return nil, err
}
return left[int(start) : int(end)+1], nil
return stringSlice(left, int(start), int(end)), nil
}
if isNumber(resultRight) {
idx, err := toNumber(ast, resultRight)
Expand All @@ -230,12 +231,12 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) {
}
left := toString(resultLeft)
if idx < 0 {
idx += float64(len(left))
idx += float64(stringLength(left))
}
if err := checkBounds(ast, left, int(idx)); err != nil {
return nil, err
}
return string(left[int(idx)]), nil
return stringIndex(left, int(idx)), nil
Comment thread
danielgtaylor marked this conversation as resolved.
}
return nil, NewError(ast.Offset, ast.Length, "array index must be number or slice %v", resultRight)
case NodeSlice:
Expand All @@ -247,9 +248,7 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) {
if err != nil {
return nil, err
}
ast.Value.([]any)[0] = resultLeft
ast.Value.([]any)[1] = resultRight
return ast.Value, nil
return []any{resultLeft, resultRight}, nil
Comment thread
danielgtaylor marked this conversation as resolved.
Comment thread
danielgtaylor marked this conversation as resolved.
case NodeLiteral:
return ast.Value, nil
case NodeSign:
Expand Down Expand Up @@ -313,7 +312,7 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) {
return math.Pow(left, right), nil
}
}
return nil, NewError(ast.Offset, ast.Length, "cannot add incompatible types %v and %v", resultLeft, resultRight)
return nil, NewError(ast.Offset, ast.Length, "cannot operate on incompatible types %v and %v", resultLeft, resultRight)
case NodeEqual, NodeNotEqual, NodeLessThan, NodeLessThanEqual, NodeGreaterThan, NodeGreaterThanEqual:
resultLeft, err := i.run(ast.Left, value)
if err != nil {
Expand Down Expand Up @@ -358,23 +357,22 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) {
switch ast.Type {
case NodeAnd:
if !left {
return left, nil
return false, nil
}
resultRight, err := i.run(ast.Right, value)
if err != nil {
return nil, err
}
return toBool(resultRight), nil
case NodeOr:
if left {
return left, nil
return true, nil
}
}
resultRight, err := i.run(ast.Right, value)
if err != nil {
return nil, err
}
right := toBool(resultRight)
switch ast.Type {
case NodeAnd:
return left && right, nil
case NodeOr:
return left || right, nil
resultRight, err := i.run(ast.Right, value)
if err != nil {
return nil, err
}
return toBool(resultRight), nil
}
case NodeBefore, NodeAfter:
resultLeft, err := i.run(ast.Left, value)
Expand Down Expand Up @@ -418,16 +416,12 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) {
return false, nil
}
if m, ok := resultRight.(map[string]any); ok {
if m[toString(resultLeft)] != nil {
return true, nil
}
return false, nil
_, ok := m[toString(resultLeft)]
return ok, nil
}
if m, ok := resultRight.(map[any]any); ok {
if m[resultLeft] != nil {
return true, nil
}
return false, nil
_, ok := m[resultLeft]
return ok, nil
}
return strings.Contains(toString(resultRight), toString(resultLeft)), nil
case NodeContains:
Expand All @@ -440,16 +434,12 @@ func (i *interpreter) run(ast *Node, value any) (any, Error) {
return false, nil
}
if m, ok := resultLeft.(map[string]any); ok {
if m[toString(resultRight)] != nil {
return true, nil
}
return false, nil
_, ok := m[toString(resultRight)]
return ok, nil
}
if m, ok := resultLeft.(map[any]any); ok {
if m[resultRight] != nil {
return true, nil
}
return false, nil
_, ok := m[resultRight]
return ok, nil
}
return strings.Contains(toString(resultLeft), toString(resultRight)), nil
case NodeStartsWith:
Expand Down
Loading
Loading