Started working on builtins

This commit is contained in:
Leon Mika 2024-05-04 10:59:17 +10:00
parent cf3a12bf0d
commit 78720eeb5b
9 changed files with 196 additions and 2 deletions

View File

@ -5,6 +5,7 @@ import (
"github.com/chzyer/readline" "github.com/chzyer/readline"
"log" "log"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
"ucl.lmika.dev/ucl/builtins"
) )
func main() { func main() {
@ -14,7 +15,10 @@ func main() {
} }
defer rl.Close() defer rl.Close()
inst := ucl.New() inst := ucl.New(
ucl.WithModule(builtins.OS()),
ucl.WithModule(builtins.FS(nil)),
)
ctx := context.Background() ctx := context.Background()
for { for {

View File

@ -83,7 +83,7 @@ var scanner = lexer.MustStateful(lexer.Rules{
{"RC", `\}`, nil}, {"RC", `\}`, nil},
{"NL", `[;\n][; \n\t]*`, nil}, {"NL", `[;\n][; \n\t]*`, nil},
{"PIPE", `\|`, nil}, {"PIPE", `\|`, nil},
{"Ident", `[-]*[a-zA-Z_][\w-]*`, nil}, {"Ident", `[-]*[a-zA-Z_:][\w-:]*`, nil},
}, },
}) })
var parser = participle.MustBuild[astScript](participle.Lexer(scanner), var parser = participle.MustBuild[astScript](participle.Lexer(scanner),

56
ucl/builtins/fs.go Normal file
View File

@ -0,0 +1,56 @@
package builtins
import (
"bufio"
"context"
"io/fs"
"os"
"ucl.lmika.dev/ucl"
)
type fsHandlers struct {
fs fs.FS
}
func FS(fs fs.FS) ucl.Module {
fsh := fsHandlers{fs: fs}
return ucl.Module{
Name: "fs",
Builtins: map[string]ucl.BuiltinHandler{
"lines": fsh.lines,
},
}
}
func (fh fsHandlers) openFile(name string) (fs.File, error) {
if fh.fs == nil {
return os.Open(name)
}
return fh.fs.Open(name)
}
func (fh fsHandlers) lines(ctx context.Context, args ucl.CallArgs) (any, error) {
var fname string
if err := args.Bind(&fname); err != nil {
return nil, err
}
f, err := fh.openFile(fname)
if err != nil {
return nil, err
}
defer f.Close()
lines := make([]string, 0)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, err
}
return lines, nil
}

37
ucl/builtins/fs_test.go Normal file
View File

@ -0,0 +1,37 @@
package builtins_test
import (
"context"
"github.com/stretchr/testify/assert"
"testing"
"testing/fstest"
"ucl.lmika.dev/ucl"
"ucl.lmika.dev/ucl/builtins"
)
var testFS = fstest.MapFS{
"test.txt": &fstest.MapFile{
Data: []byte("these\nare\nlines"),
},
}
func TestFS_Cat(t *testing.T) {
tests := []struct {
descr string
eval string
want any
}{
{descr: "read file", eval: `fs:lines "test.txt"`, want: []string{"these", "are", "lines"}},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
inst := ucl.New(
ucl.WithModule(builtins.FS(testFS)),
)
res, err := inst.Eval(context.Background(), tt.eval)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
}
}

40
ucl/builtins/os.go Normal file
View File

@ -0,0 +1,40 @@
package builtins
import (
"context"
"os"
"ucl.lmika.dev/ucl"
)
type osHandlers struct {
}
func OS() ucl.Module {
osh := osHandlers{}
return ucl.Module{
Name: "os",
Builtins: map[string]ucl.BuiltinHandler{
"env": osh.env,
},
}
}
func (oh osHandlers) env(ctx context.Context, args ucl.CallArgs) (any, error) {
var envName string
if err := args.Bind(&envName); err != nil {
return nil, err
}
val, ok := os.LookupEnv(envName)
if ok {
return val, nil
}
var defValue any
if err := args.Bind(&defValue); err == nil {
return defValue, nil
}
return "", nil
}

36
ucl/builtins/os_test.go Normal file
View File

@ -0,0 +1,36 @@
package builtins_test
import (
"context"
"github.com/stretchr/testify/assert"
"testing"
"ucl.lmika.dev/ucl"
"ucl.lmika.dev/ucl/builtins"
)
func TestOS_Env(t *testing.T) {
tests := []struct {
descr string
eval string
want any
}{
{descr: "env value", eval: `os:env "MY_ENV"`, want: "my env value"},
{descr: "missing env value", eval: `os:env "MISSING_THING"`, want: ""},
{descr: "default env value (str)", eval: `os:env "MISSING_THING" "my default"`, want: "my default"},
{descr: "default env value (int)", eval: `os:env "MISSING_THING" 1352`, want: 1352},
{descr: "default env value (nil)", eval: `os:env "MISSING_THING" ()`, want: nil},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
t.Setenv("MY_ENV", "my env value")
inst := ucl.New(
ucl.WithModule(builtins.OS()),
)
res, err := inst.Eval(context.Background(), tt.eval)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
}
}

View File

@ -20,6 +20,12 @@ func displayResult(ctx context.Context, inst *Inst, res object) (err error) {
if _, err = fmt.Fprintln(inst.out, "(nil)"); err != nil { if _, err = fmt.Fprintln(inst.out, "(nil)"); err != nil {
return err return err
} }
case listable:
for i := 0; i < v.Len(); i++ {
if err = displayResult(ctx, inst, v.Index(i)); err != nil {
return err
}
}
default: default:
if _, err = fmt.Fprintln(inst.out, v.String()); err != nil { if _, err = fmt.Fprintln(inst.out, v.String()); err != nil {
return err return err

View File

@ -29,6 +29,19 @@ func WithMissingBuiltinHandler(handler MissingBuiltinHandler) InstOption {
} }
} }
func WithModule(module Module) InstOption {
return func(i *Inst) {
for name, builtin := range module.Builtins {
i.SetBuiltin(module.Name+":"+name, builtin)
}
}
}
type Module struct {
Name string
Builtins map[string]BuiltinHandler
}
func New(opts ...InstOption) *Inst { func New(opts ...InstOption) *Inst {
rootEC := &evalCtx{} rootEC := &evalCtx{}
rootEC.root = rootEC rootEC.root = rootEC

View File

@ -94,6 +94,8 @@ func (u userBuiltin) invoke(ctx context.Context, args invocationArgs) (object, e
func bindArg(v interface{}, arg object) error { func bindArg(v interface{}, arg object) error {
switch t := v.(type) { switch t := v.(type) {
case *interface{}:
*t, _ = toGoValue(arg)
case *string: case *string:
*t = arg.String() *t = arg.String()
case *int: case *int: