Skip to content

Commit

Permalink
File load, key lookup, and switch statement
Browse files Browse the repository at this point in the history
  • Loading branch information
zix99 committed May 17, 2024
1 parent fddc4e6 commit 866742b
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 0 deletions.
5 changes: 5 additions & 0 deletions cmd/testdata/lookup.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#test 22
bob 33
jill 44

cat 55
40 changes: 40 additions & 0 deletions docs/usage/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ Syntax: `{if val ifTrue ifFalse}`, `{if val ifTrue}`, `{unless val ifFalse}`

If `val` is truthy, then return `ifTrue` else optionally return `ifFalse`

#### Switch

Syntax: `{switch ifTrue val ifTrue val ... [ifFalseVal]}`

In pairs, if a given value is truthy, return the value immediately after. If
there is an odd number of arguments, the last value is used as the "else" result.
Otherwise, empty string is returned.

#### Equals, NotEquals, Not

Syntax: `{eq a b}`, `{neq a b}`, `{not a}`
Expand Down Expand Up @@ -259,6 +267,38 @@ to form arrays that have meaning for a given aggregator.

Specifying multiple expressions is equivalent, eg. `{$ a b}` is the same as `-e a -e b`

### File Loading and Lookup Tables

Load external static content as either raw string, or to be used to lookup
a value given a key.

#### Load

Syntax: `{load "filename"}`

Loads a given filename as text.

#### Lookup, HasKey

Syntax: `{lookup key "kv-pairs" ["commentPrefix"]}`, `{haskey key "kv-pairs" ["commentPrefix"]}`

Given a set of kv-pairs (eg. from a loaded file), lookup a key. For `lookup` return a value
and for `haskey` return truthy or falsey.

If a `commentPrefix` is provided, lines in lookup text are ignored if they start with the prefix.

Example kv-pairs text. Keys and values are separated by any whitespace.

```
key1 val1
key2 val2
#comment if '#' set as prefix
key3 val3
#blank lines are ignored
too many values are also ignored
```

### Ranges (Arrays)

Range functions provide the ability to work with arrays in expressions. You
Expand Down
6 changes: 6 additions & 0 deletions pkg/expressions/stdlib/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ var StandardFunctions = map[string]KeyBuilderFunction{

// Comparisons
"if": KeyBuilderFunction(kfIf),
"switch": kfSwitch,
"unless": KeyBuilderFunction(kfUnless),
"eq": stringComparator(func(a, b string) string {
if a == b {
Expand Down Expand Up @@ -94,6 +95,11 @@ var StandardFunctions = map[string]KeyBuilderFunction{
"dirname": kfPathDir,
"extname": kfPathExt,

// File operations
"load": kfLoadFile,
"lookup": kfLookupKey,
"haskey": kfHasKey,

// Formatting
"hi": KeyBuilderFunction(kfHumanizeInt),
"hf": KeyBuilderFunction(kfHumanizeFloat),
Expand Down
19 changes: 19 additions & 0 deletions pkg/expressions/stdlib/funcsComparators.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,25 @@ func kfIf(args []KeyBuilderStage) (KeyBuilderStage, error) {
}), nil
}

// {switch ifTrue val ifTrue val ... [ifFalseVal]}
func kfSwitch(args []KeyBuilderStage) (KeyBuilderStage, error) {
if len(args) <= 1 {
return stageErrArgRange(args, "2+")
}

return func(context KeyBuilderContext) string {
for i := 0; i+1 < len(args); i += 2 {
if Truthy(args[i](context)) {
return args[i+1](context)
}
}
if len(args)%2 == 1 {
return args[len(args)-1](context)
}
return ""
}, nil
}

func kfUnless(args []KeyBuilderStage) (KeyBuilderStage, error) {
if len(args) != 2 {
return stageErrArgCount(args, 2)
Expand Down
9 changes: 9 additions & 0 deletions pkg/expressions/stdlib/funcsComparators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ func TestIfStatement(t *testing.T) {
testExpression(t, mockContext(), `{if {neq "" ""} true false}`, "false")
}

func TestSwitch(t *testing.T) {
testExpression(t, mockContext("a"), "{switch {eq {0} a} isa {eq {0} b} isb 1 null}", "isa")
testExpression(t, mockContext("b"), "{switch {eq {0} a} isa {eq {0} b} isb 1 null}", "isb")
testExpression(t, mockContext("c"), "{switch {eq {0} a} isa {eq {0} b} isb 1 null}", "null")
testExpression(t, mockContext("c"), "{switch {eq {0} a} isa {eq {0} b} isb null}", "null")
testExpression(t, mockContext("a"), "{switch {eq {0} a} isa {eq {0} b} isb 1}", "isa")
testExpressionErr(t, mockContext("a"), "{switch {eq {0} a}}", "<ARGN>", ErrArgCount)
}

func TestUnlessStatement(t *testing.T) {
testExpression(t, mockContext("abc"), `{unless {1} {0}} {unless abc efg} {unless "" bob}`, "abc bob")
testExpressionErr(t, mockContext("abc"), `{unless joe}`, "<ARGN>", ErrArgCount)
Expand Down
105 changes: 105 additions & 0 deletions pkg/expressions/stdlib/lookups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package stdlib

import (
"bufio"
"io"
"os"
"rare/pkg/expressions"
"strings"
)

// {load "filename"}
// loads static file as string
func kfLoadFile(args []expressions.KeyBuilderStage) (expressions.KeyBuilderStage, error) {
if len(args) != 1 {
return stageErrArgCount(args, 1)
}

filename, ok := expressions.EvalStaticStage(args[0])
if !ok {
return stageError(ErrConst)
}

f, err := os.Open(filename)
if err != nil {
return stageError(newFuncErr("<FILE>", "Unable to open file: "+filename))
}
defer f.Close()

content, err := io.ReadAll(f)
if err != nil {
return stageError(newFuncErr("<FILE>", "Error reading file: "+filename))
}

sContent := string(content)

return func(context expressions.KeyBuilderContext) string {
return sContent
}, nil
}

func buildLookupTable(content string, commentPrefix string) map[string]string {
lookup := make(map[string]string)
scanner := bufio.NewScanner(strings.NewReader(content))

for scanner.Scan() {
line := scanner.Text()

if commentPrefix != "" && strings.HasPrefix(line, commentPrefix) {
continue
}

parts := strings.Fields(line)
switch len(parts) {
case 0: //noop
case 1:
lookup[parts[0]] = ""
case 2:
lookup[parts[0]] = parts[1]
}
}
return lookup
}

// {lookup key "table" [commentPrefix]}
func kfLookupKey(args []expressions.KeyBuilderStage) (expressions.KeyBuilderStage, error) {
if !isArgCountBetween(args, 2, 3) {
return stageErrArgRange(args, "2-3")
}

content, ok := expressions.EvalStaticStage(args[1])
if !ok {
return stageArgError(ErrConst, 1)
}

commentPrefix := expressions.EvalStageIndexOrDefault(args, 2, "")

lookup := buildLookupTable(content, commentPrefix)

return func(context expressions.KeyBuilderContext) string {
key := args[0](context)
return lookup[key]
}, nil
}

// {haskey key "table" [commentprefix]}
func kfHasKey(args []expressions.KeyBuilderStage) (expressions.KeyBuilderStage, error) {
if len(args) != 2 {
return stageErrArgCount(args, 2)
}

content, ok := expressions.EvalStaticStage(args[1])
if !ok {
return stageArgError(ErrConst, 1)
}

commentPrefix := expressions.EvalStageIndexOrDefault(args, 2, "")

lookup := buildLookupTable(content, commentPrefix)

return func(context expressions.KeyBuilderContext) string {
key := args[0](context)
_, has := lookup[key]
return expressions.TruthyStr(has)
}, nil
}
19 changes: 19 additions & 0 deletions pkg/expressions/stdlib/lookups_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package stdlib

import "testing"

func TestLoadFile(t *testing.T) {
testExpression(t, mockContext(), "{load ../../../cmd/testdata/graph.txt}", "bob 22\njack 93\njill 3\nmaria 19")
testExpressionErr(t, mockContext(), "{load {0}}", "<CONST>", ErrConst)
}

func TestLookup(t *testing.T) {
testExpression(t, mockContext("bob"), "{lookup {0} {load ../../../cmd/testdata/lookup.txt}}", "33")
testExpression(t, mockContext("bobert"), "{lookup {0} {load ../../../cmd/testdata/lookup.txt}}", "")
testExpression(t, mockContext("#test"), "{lookup {0} {load ../../../cmd/testdata/lookup.txt}}", "22")
testExpression(t, mockContext("#test"), "{lookup {0} {load ../../../cmd/testdata/lookup.txt} \"#\"}", "")
}

func BenchmarkLoadFile(b *testing.B) {
benchmarkExpression(b, mockContext(), "{load ../../../cmd/testdata/graph.txt}", "bob 22\njack 93\njill 3\nmaria 19")
}
18 changes: 18 additions & 0 deletions pkg/expressions/stdlib/testutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,21 @@ func testExpressionErr(t *testing.T, context KeyBuilderContext, expression strin
}
}
}

// benchmark an expreession, as a sub-benchmark. Checks value before running test
func benchmarkExpression(b *testing.B, context KeyBuilderContext, expression, expected string) {
kb, err := NewStdKeyBuilderEx(false).Compile(expression)
if err != nil {
b.Fatal(err)
}

if s := kb.BuildKey(context); s != expected {
b.Fatalf("%s != %s", s, expected)
}

b.Run(expression, func(b *testing.B) {
for i := 0; i < b.N; i++ {
kb.BuildKey(context)
}
})
}
7 changes: 7 additions & 0 deletions pkg/expressions/truthy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ const FalsyVal = ""
func Truthy(s string) bool {
return strings.TrimSpace(s) != FalsyVal
}

func TruthyStr(is bool) string {
if is {
return TruthyVal
}
return FalsyVal
}

0 comments on commit 866742b

Please sign in to comment.