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
21
ucl/ast.go
21
ucl/ast.go
|
@ -13,12 +13,23 @@ type astStringStringSpan struct {
|
|||
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 {
|
||||
Pos lexer.Position
|
||||
Chars *string `parser:"@Char"`
|
||||
Escaped *string `parser:"| @Escaped"`
|
||||
IdentRef *string `parser:"| @IdentRef"`
|
||||
LongIdentRef *string `parser:"| @LongIdentRef"`
|
||||
LongIdentRef *astLongIdent `parser:"| LongIdentRef @@ LIEnd"`
|
||||
SubExpr *astPipeline `parser:"| StartSubExpr @@ RP"`
|
||||
}
|
||||
|
||||
|
@ -134,10 +145,16 @@ var scanner = lexer.MustStateful(lexer.Rules{
|
|||
{"Escaped", `\\.`, nil},
|
||||
{"StringEnd", `"`, lexer.Pop()},
|
||||
{"IdentRef", `\$[-]*[a-zA-Z_][\w-]*`, nil},
|
||||
{"LongIdentRef", `\$[{][^}]*[}]`, nil},
|
||||
{"LongIdentRef", `\$[{]`, lexer.Push("LongIdent")},
|
||||
{"StartSubExpr", `\$[(]`, lexer.Push("Root")},
|
||||
{"Char", `[^$"\\]+`, nil},
|
||||
},
|
||||
"LongIdent": {
|
||||
{"LIIdent", `[-]*[a-zA-Z_][\w-]*`, nil},
|
||||
{"LIDot", `[.]`, nil},
|
||||
{"LILp", `\(`, lexer.Push("Root")},
|
||||
{"LIEnd", `\}`, lexer.Pop()},
|
||||
},
|
||||
"SingleString": {
|
||||
{"SingleStringEnd", `'`, lexer.Pop()},
|
||||
{"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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -299,10 +300,11 @@ func (e evaluator) interpolateDoubleQuotedString(ctx context.Context, ec *evalCt
|
|||
sb.WriteString(v.String())
|
||||
}
|
||||
case n.LongIdentRef != nil:
|
||||
identVal := (*n.LongIdentRef)[2 : len(*n.LongIdentRef)-1]
|
||||
if v, ok := ec.getVar(identVal); ok && v != nil {
|
||||
sb.WriteString(v.String())
|
||||
v, err := e.interpolateLongIdent(ctx, ec, n.LongIdentRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.WriteString(v)
|
||||
case n.SubExpr != nil:
|
||||
res, err := e.evalPipeline(ctx, ec, n.SubExpr)
|
||||
if err != nil {
|
||||
|
@ -316,6 +318,38 @@ func (e evaluator) interpolateDoubleQuotedString(ctx context.Context, ec *evalCt
|
|||
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) {
|
||||
pipelineRes, err := e.evalPipeline(ctx, ec, n)
|
||||
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 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 4", expr: `set "crazy var" "unknown" ; firstarg "hello ${crazy var}"`, want: "hello unknown"},
|
||||
{desc: "interpolate string 5", expr: `set what "world" ; firstarg "hello $($what)"`, want: "hello world"},
|
||||
{desc: "interpolate string 6", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"},
|
||||
{desc: "interpolate string 7", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"},
|
||||
{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 4", expr: `set crazy [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"},
|
||||
{desc: "interpolate string 5", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"},
|
||||
{desc: "interpolate string 6", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"},
|
||||
{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: `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
|
||||
{desc: "sub expression 1", expr: `firstarg (sjoin "hello")`, want: "hello"},
|
||||
|
|
15
ucl/objs.go
15
ucl/objs.go
|
@ -7,6 +7,7 @@ import (
|
|||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lmika/gopkgs/fp/slices"
|
||||
)
|
||||
|
@ -124,6 +125,16 @@ func (b boolObject) Truthy() bool {
|
|||
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) {
|
||||
switch v := obj.(type) {
|
||||
case OpaqueObject:
|
||||
|
@ -136,6 +147,8 @@ func toGoValue(obj Object) (interface{}, bool) {
|
|||
return int(v), true
|
||||
case boolObject:
|
||||
return bool(v), true
|
||||
case timeObject:
|
||||
return time.Time(v), true
|
||||
case listObject:
|
||||
xs := make([]interface{}, 0, len(v))
|
||||
for _, va := range v {
|
||||
|
@ -181,6 +194,8 @@ func fromGoValue(v any) (Object, error) {
|
|||
return intObject(t), nil
|
||||
case bool:
|
||||
return boolObject(t), nil
|
||||
case time.Time:
|
||||
return timeObject(t), nil
|
||||
}
|
||||
|
||||
return fromGoReflectValue(reflect.ValueOf(v))
|
||||
|
|
Loading…
Reference in a new issue