Added subscript support for long var interpolation
- Modified long var interpolation to support dot lookups - Added a time:from-unix function and added time.Time as an object
This commit is contained in:
parent
1632a0a294
commit
fc43c2ce7d
29
ucl/ast.go
29
ucl/ast.go
|
@ -13,13 +13,24 @@ type astStringStringSpan struct {
|
||||||
Chars *string `parser:"@SingleChar"`
|
Chars *string `parser:"@SingleChar"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type astLongIdentDotSuffix struct {
|
||||||
|
KeyName *string `parser:"@LIIdent"`
|
||||||
|
Pipeline *astPipeline `parser:"| LILp @@ RP"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type astLongIdent struct {
|
||||||
|
Pos lexer.Position
|
||||||
|
VarName string `parser:"@LIIdent"`
|
||||||
|
DotSuffix []astLongIdentDotSuffix `parser:"( LIDot @@ )*"`
|
||||||
|
}
|
||||||
|
|
||||||
type astDoubleStringSpan struct {
|
type astDoubleStringSpan struct {
|
||||||
Pos lexer.Position
|
Pos lexer.Position
|
||||||
Chars *string `parser:"@Char"`
|
Chars *string `parser:"@Char"`
|
||||||
Escaped *string `parser:"| @Escaped"`
|
Escaped *string `parser:"| @Escaped"`
|
||||||
IdentRef *string `parser:"| @IdentRef"`
|
IdentRef *string `parser:"| @IdentRef"`
|
||||||
LongIdentRef *string `parser:"| @LongIdentRef"`
|
LongIdentRef *astLongIdent `parser:"| LongIdentRef @@ LIEnd"`
|
||||||
SubExpr *astPipeline `parser:"| StartSubExpr @@ RP"`
|
SubExpr *astPipeline `parser:"| StartSubExpr @@ RP"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type astDoubleString struct {
|
type astDoubleString struct {
|
||||||
|
@ -134,10 +145,16 @@ var scanner = lexer.MustStateful(lexer.Rules{
|
||||||
{"Escaped", `\\.`, nil},
|
{"Escaped", `\\.`, nil},
|
||||||
{"StringEnd", `"`, lexer.Pop()},
|
{"StringEnd", `"`, lexer.Pop()},
|
||||||
{"IdentRef", `\$[-]*[a-zA-Z_][\w-]*`, nil},
|
{"IdentRef", `\$[-]*[a-zA-Z_][\w-]*`, nil},
|
||||||
{"LongIdentRef", `\$[{][^}]*[}]`, nil},
|
{"LongIdentRef", `\$[{]`, lexer.Push("LongIdent")},
|
||||||
{"StartSubExpr", `\$[(]`, lexer.Push("Root")},
|
{"StartSubExpr", `\$[(]`, lexer.Push("Root")},
|
||||||
{"Char", `[^$"\\]+`, nil},
|
{"Char", `[^$"\\]+`, nil},
|
||||||
},
|
},
|
||||||
|
"LongIdent": {
|
||||||
|
{"LIIdent", `[-]*[a-zA-Z_][\w-]*`, nil},
|
||||||
|
{"LIDot", `[.]`, nil},
|
||||||
|
{"LILp", `\(`, lexer.Push("Root")},
|
||||||
|
{"LIEnd", `\}`, lexer.Pop()},
|
||||||
|
},
|
||||||
"SingleString": {
|
"SingleString": {
|
||||||
{"SingleStringEnd", `'`, lexer.Pop()},
|
{"SingleStringEnd", `'`, lexer.Pop()},
|
||||||
{"SingleChar", `[^']+`, nil},
|
{"SingleChar", `[^']+`, nil},
|
||||||
|
|
26
ucl/builtins/time.go
Normal file
26
ucl/builtins/time.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package builtins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
"ucl.lmika.dev/ucl"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Time() ucl.Module {
|
||||||
|
return ucl.Module{
|
||||||
|
Name: "time",
|
||||||
|
Builtins: map[string]ucl.BuiltinHandler{
|
||||||
|
"from-unix": timeFromUnix,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeFromUnix(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||||
|
var ux int
|
||||||
|
|
||||||
|
if err := args.Bind(&ux); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Unix(int64(ux), 0).UTC(), nil
|
||||||
|
}
|
37
ucl/builtins/time_test.go
Normal file
37
ucl/builtins/time_test.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package builtins_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"ucl.lmika.dev/ucl"
|
||||||
|
"ucl.lmika.dev/ucl/builtins"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTime_FromUnix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
eval string
|
||||||
|
want any
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{desc: "from unix 1", eval: `time:from-unix 0`, want: time.Unix(0, 0).UTC()},
|
||||||
|
{desc: "from unix 2", eval: `time:from-unix 0 | cat`, want: "1970-01-01T00:00:00Z"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
inst := ucl.New(
|
||||||
|
ucl.WithModule(builtins.Time()),
|
||||||
|
)
|
||||||
|
res, err := inst.Eval(context.Background(), tt.eval)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
40
ucl/eval.go
40
ucl/eval.go
|
@ -3,6 +3,7 @@ package ucl
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -299,10 +300,11 @@ func (e evaluator) interpolateDoubleQuotedString(ctx context.Context, ec *evalCt
|
||||||
sb.WriteString(v.String())
|
sb.WriteString(v.String())
|
||||||
}
|
}
|
||||||
case n.LongIdentRef != nil:
|
case n.LongIdentRef != nil:
|
||||||
identVal := (*n.LongIdentRef)[2 : len(*n.LongIdentRef)-1]
|
v, err := e.interpolateLongIdent(ctx, ec, n.LongIdentRef)
|
||||||
if v, ok := ec.getVar(identVal); ok && v != nil {
|
if err != nil {
|
||||||
sb.WriteString(v.String())
|
return nil, err
|
||||||
}
|
}
|
||||||
|
sb.WriteString(v)
|
||||||
case n.SubExpr != nil:
|
case n.SubExpr != nil:
|
||||||
res, err := e.evalPipeline(ctx, ec, n.SubExpr)
|
res, err := e.evalPipeline(ctx, ec, n.SubExpr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -316,6 +318,38 @@ func (e evaluator) interpolateDoubleQuotedString(ctx context.Context, ec *evalCt
|
||||||
return StringObject(sb.String()), nil
|
return StringObject(sb.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e evaluator) interpolateLongIdent(ctx context.Context, ec *evalCtx, n *astLongIdent) (_ string, err error) {
|
||||||
|
res, ok := ec.getVar(n.VarName)
|
||||||
|
if !ok {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dot := range n.DotSuffix {
|
||||||
|
if res == nil {
|
||||||
|
return "", errorWithPos{fmt.Errorf("attempt to get field from nil value '%v'", n.VarName), n.Pos}
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx Object
|
||||||
|
if dot.KeyName != nil {
|
||||||
|
idx = StringObject(*dot.KeyName)
|
||||||
|
} else {
|
||||||
|
idx, err = e.evalPipeline(ctx, ec, dot.Pipeline)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err = indexLookup(ctx, res, idx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return res.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e evaluator) evalSub(ctx context.Context, ec *evalCtx, n *astPipeline) (Object, error) {
|
func (e evaluator) evalSub(ctx context.Context, ec *evalCtx, n *astPipeline) (Object, error) {
|
||||||
pipelineRes, err := e.evalPipeline(ctx, ec, n)
|
pipelineRes, err := e.evalPipeline(ctx, ec, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -25,11 +25,15 @@ func TestInst_Eval(t *testing.T) {
|
||||||
{desc: "interpolate string 1", expr: `set what "world" ; firstarg "hello $what"`, want: "hello world"},
|
{desc: "interpolate string 1", expr: `set what "world" ; firstarg "hello $what"`, want: "hello world"},
|
||||||
{desc: "interpolate string 2", expr: `set what "world" ; set when "now" ; firstarg "$when, hello $what"`, want: "now, hello world"},
|
{desc: "interpolate string 2", expr: `set what "world" ; set when "now" ; firstarg "$when, hello $what"`, want: "now, hello world"},
|
||||||
{desc: "interpolate string 3", expr: `set what "world" ; set when "now" ; firstarg "${when}, hello ${what}"`, want: "now, hello world"},
|
{desc: "interpolate string 3", expr: `set what "world" ; set when "now" ; firstarg "${when}, hello ${what}"`, want: "now, hello world"},
|
||||||
{desc: "interpolate string 4", expr: `set "crazy var" "unknown" ; firstarg "hello ${crazy var}"`, want: "hello unknown"},
|
{desc: "interpolate string 4", expr: `set crazy [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"},
|
||||||
{desc: "interpolate string 5", expr: `set what "world" ; firstarg "hello $($what)"`, want: "hello world"},
|
{desc: "interpolate string 5", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"},
|
||||||
{desc: "interpolate string 6", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"},
|
{desc: "interpolate string 6", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"},
|
||||||
{desc: "interpolate string 7", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"},
|
{desc: "interpolate string 7", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 2 | sub (sub 2 1) | sub 1)}"`, want: "hello hither"},
|
||||||
{desc: "interpolate string 8", expr: `firstarg ("$(add 2 (add 1 1)) + $([1 2 3].(1) | cat ("$("")")) = $(("$(add 2 (4))"))")`, want: "4 + 2 = 6"},
|
{desc: "interpolate string 8", expr: `set words ["old": ["hither" "thither" "yonder"] "new": ["near" "far"]] ; firstarg "hello ${words.old.(2)}"`, want: "hello yonder"},
|
||||||
|
{desc: "interpolate string 9", expr: `set what "world" ; firstarg "hello $($what)"`, want: "hello world"},
|
||||||
|
{desc: "interpolate string 10", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"},
|
||||||
|
{desc: "interpolate string 11", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"},
|
||||||
|
{desc: "interpolate string 12", expr: `firstarg ("$(add 2 (add 1 1)) + $([1 2 3].(1) | cat ("$("")")) = $(("$(add 2 (4))"))")`, want: "4 + 2 = 6"},
|
||||||
|
|
||||||
// Sub-expressions
|
// Sub-expressions
|
||||||
{desc: "sub expression 1", expr: `firstarg (sjoin "hello")`, want: "hello"},
|
{desc: "sub expression 1", expr: `firstarg (sjoin "hello")`, want: "hello"},
|
||||||
|
|
15
ucl/objs.go
15
ucl/objs.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/lmika/gopkgs/fp/slices"
|
"github.com/lmika/gopkgs/fp/slices"
|
||||||
)
|
)
|
||||||
|
@ -124,6 +125,16 @@ func (b boolObject) Truthy() bool {
|
||||||
return bool(b)
|
return bool(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type timeObject time.Time
|
||||||
|
|
||||||
|
func (t timeObject) String() string {
|
||||||
|
return time.Time(t).Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t timeObject) Truthy() bool {
|
||||||
|
return !time.Time(t).IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
func toGoValue(obj Object) (interface{}, bool) {
|
func toGoValue(obj Object) (interface{}, bool) {
|
||||||
switch v := obj.(type) {
|
switch v := obj.(type) {
|
||||||
case OpaqueObject:
|
case OpaqueObject:
|
||||||
|
@ -136,6 +147,8 @@ func toGoValue(obj Object) (interface{}, bool) {
|
||||||
return int(v), true
|
return int(v), true
|
||||||
case boolObject:
|
case boolObject:
|
||||||
return bool(v), true
|
return bool(v), true
|
||||||
|
case timeObject:
|
||||||
|
return time.Time(v), true
|
||||||
case listObject:
|
case listObject:
|
||||||
xs := make([]interface{}, 0, len(v))
|
xs := make([]interface{}, 0, len(v))
|
||||||
for _, va := range v {
|
for _, va := range v {
|
||||||
|
@ -181,6 +194,8 @@ func fromGoValue(v any) (Object, error) {
|
||||||
return intObject(t), nil
|
return intObject(t), nil
|
||||||
case bool:
|
case bool:
|
||||||
return boolObject(t), nil
|
return boolObject(t), nil
|
||||||
|
case time.Time:
|
||||||
|
return timeObject(t), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromGoReflectValue(reflect.ValueOf(v))
|
return fromGoReflectValue(reflect.ValueOf(v))
|
||||||
|
|
Loading…
Reference in a new issue