mirror of
https://github.com/google/go-jsonnet.git
synced 2025-09-28 17:01:02 +02:00
Support for native callbacks
This commit is contained in:
parent
96a2abc46c
commit
ed281bc563
17
builtins.go
17
builtins.go
@ -589,13 +589,27 @@ func builtinExtVar(e *evaluator, namep potentialValue) (value, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
index := ast.Identifier(name.getString())
|
||||
index := name.getString()
|
||||
if pv, ok := e.i.extVars[index]; ok {
|
||||
return e.evaluate(pv)
|
||||
}
|
||||
return nil, e.Error("Undefined external variable: " + string(index))
|
||||
}
|
||||
|
||||
func builtinNative(e *evaluator, namep potentialValue) (value, error) {
|
||||
name, err := e.evaluateString(namep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
index := name.getString()
|
||||
if f, exists := e.i.nativeFuncs[index]; exists {
|
||||
return &valueFunction{ec: f}, nil
|
||||
|
||||
}
|
||||
return nil, e.Error(fmt.Sprintf("Unrecognized native function name: %v", index))
|
||||
|
||||
}
|
||||
|
||||
type unaryBuiltin func(*evaluator, potentialValue) (value, error)
|
||||
type binaryBuiltin func(*evaluator, potentialValue, potentialValue) (value, error)
|
||||
type ternaryBuiltin func(*evaluator, potentialValue, potentialValue, potentialValue) (value, error)
|
||||
@ -772,6 +786,7 @@ var funcBuiltins = buildBuiltinMap([]builtin{
|
||||
&BinaryBuiltin{name: "pow", function: builtinPow, parameters: ast.Identifiers{"base", "exp"}},
|
||||
&BinaryBuiltin{name: "modulo", function: builtinModulo, parameters: ast.Identifiers{"x", "y"}},
|
||||
&UnaryBuiltin{name: "md5", function: builtinMd5, parameters: ast.Identifiers{"x"}},
|
||||
&UnaryBuiltin{name: "native", function: builtinNative, parameters: ast.Identifiers{"x"}},
|
||||
|
||||
// internal
|
||||
&UnaryBuiltin{name: "$objectFlatMerge", function: builtinUglyObjectFlatMerge, parameters: ast.Identifiers{"x"}},
|
||||
|
@ -298,6 +298,12 @@ func desugar(astPtr *ast.Node, objLevel int) (err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for i := range node.Arguments.Named {
|
||||
err = desugar(&node.Arguments.Named[i].Arg, objLevel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.ApplyBrace:
|
||||
err = desugar(&node.Left, objLevel)
|
||||
|
@ -214,7 +214,10 @@ type interpreter struct {
|
||||
stack callStack
|
||||
|
||||
// External variables
|
||||
extVars map[ast.Identifier]potentialValue
|
||||
extVars map[string]potentialValue
|
||||
|
||||
// Native functions
|
||||
nativeFuncs map[string]*nativeFunction
|
||||
|
||||
// A part of std object common to all files
|
||||
baseStd valueObject
|
||||
@ -730,6 +733,46 @@ func (i *interpreter) manifestAndSerializeJSON(trace *TraceElement, v value, mul
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func jsonToValue(e *evaluator, v interface{}) (value, error) {
|
||||
switch v := v.(type) {
|
||||
case nil:
|
||||
return &nullValue, nil
|
||||
|
||||
case []interface{}:
|
||||
elems := make([]potentialValue, len(v))
|
||||
for i, elem := range v {
|
||||
val, err := jsonToValue(e, elem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elems[i] = &readyValue{val}
|
||||
}
|
||||
return makeValueArray(elems), nil
|
||||
|
||||
case bool:
|
||||
return makeValueBoolean(v), nil
|
||||
case float64:
|
||||
return makeValueNumber(v), nil
|
||||
|
||||
case map[string]interface{}:
|
||||
fieldMap := map[string]value{}
|
||||
for name, f := range v {
|
||||
val, err := jsonToValue(e, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fieldMap[name] = val
|
||||
}
|
||||
return buildObject(ast.ObjectFieldInherit, fieldMap), nil
|
||||
|
||||
case string:
|
||||
return makeValueString(v), nil
|
||||
|
||||
default:
|
||||
return nil, e.Error(fmt.Sprintf("Not a json type: %#+v", v))
|
||||
}
|
||||
}
|
||||
|
||||
func (i *interpreter) EvalInCleanEnv(fromWhere *TraceElement, env *environment, ast ast.Node, trimmable bool) (value, error) {
|
||||
err := i.newCall(fromWhere, *env, trimmable)
|
||||
if err != nil {
|
||||
@ -776,8 +819,8 @@ func evaluateStd(i *interpreter) (value, error) {
|
||||
return i.EvalInCleanEnv(evalTrace, &beforeStdEnv, node, false)
|
||||
}
|
||||
|
||||
func prepareExtVars(i *interpreter, ext vmExtMap, kind string) map[ast.Identifier]potentialValue {
|
||||
result := make(map[ast.Identifier]potentialValue)
|
||||
func prepareExtVars(i *interpreter, ext vmExtMap, kind string) map[string]potentialValue {
|
||||
result := make(map[string]potentialValue)
|
||||
for name, content := range ext {
|
||||
if content.isCode {
|
||||
varLoc := ast.MakeLocationRangeMessage("During evaluation")
|
||||
@ -788,9 +831,9 @@ func prepareExtVars(i *interpreter, ext vmExtMap, kind string) map[ast.Identifie
|
||||
i: i,
|
||||
trace: varTrace,
|
||||
}
|
||||
result[ast.Identifier(name)] = codeToPV(e, "<"+kind+":"+name+">", content.value)
|
||||
result[name] = codeToPV(e, "<"+kind+":"+name+">", content.value)
|
||||
} else {
|
||||
result[ast.Identifier(name)] = &readyValue{makeValueString(content.value)}
|
||||
result[name] = &readyValue{makeValueString(content.value)}
|
||||
}
|
||||
}
|
||||
return result
|
||||
@ -804,10 +847,11 @@ func buildObject(hide ast.ObjectFieldHide, fields map[string]value) valueObject
|
||||
return makeValueSimpleObject(bindingFrame{}, fieldMap, nil)
|
||||
}
|
||||
|
||||
func buildInterpreter(ext vmExtMap, maxStack int, importer Importer) (*interpreter, error) {
|
||||
func buildInterpreter(ext vmExtMap, nativeFuncs map[string]*nativeFunction, maxStack int, importer Importer) (*interpreter, error) {
|
||||
i := interpreter{
|
||||
stack: makeCallStack(maxStack),
|
||||
importCache: MakeImportCache(importer),
|
||||
nativeFuncs: nativeFuncs,
|
||||
}
|
||||
|
||||
stdObj, err := buildStdObject(&i)
|
||||
@ -834,8 +878,9 @@ func makeInitialEnv(filename string, baseStd valueObject) environment {
|
||||
)
|
||||
}
|
||||
|
||||
func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, maxStack int, importer Importer) (string, error) {
|
||||
i, err := buildInterpreter(ext, maxStack, importer)
|
||||
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
|
||||
func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*nativeFunction, maxStack int, importer Importer) (string, error) {
|
||||
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -854,7 +899,7 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, maxStack int, importer
|
||||
toplevelArgMap := prepareExtVars(i, tla, "top-level-arg")
|
||||
args := callArguments{}
|
||||
for argName, pv := range toplevelArgMap {
|
||||
args.named = append(args.named, namedCallArgument{name: argName, pv: pv})
|
||||
args.named = append(args.named, namedCallArgument{name: ast.Identifier(argName), pv: pv})
|
||||
}
|
||||
funcLoc := ast.MakeLocationRangeMessage("Top-level-function")
|
||||
funcTrace := &TraceElement{
|
||||
|
13
main_test.go
13
main_test.go
@ -18,6 +18,7 @@ package jsonnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
@ -25,6 +26,7 @@ import (
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/go-jsonnet/ast"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
@ -100,6 +102,17 @@ func TestMain(t *testing.T) {
|
||||
vm.ExtCode(name, value)
|
||||
}
|
||||
|
||||
vm.NativeFunction(&nativeFunction{
|
||||
name: "jsonToString",
|
||||
params: ast.Identifiers{"x"},
|
||||
f: func(x []interface{}) (interface{}, error) {
|
||||
bytes, err := json.Marshal(x[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(bytes), nil
|
||||
},
|
||||
})
|
||||
read := func(file string) []byte {
|
||||
bytz, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
|
1
testdata/native1.golden
vendored
Normal file
1
testdata/native1.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
"\"test\""
|
1
testdata/native1.jsonnet
vendored
Normal file
1
testdata/native1.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
std.native("jsonToString")("test")
|
1
testdata/native2.golden
vendored
Normal file
1
testdata/native2.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
"{}"
|
1
testdata/native2.jsonnet
vendored
Normal file
1
testdata/native2.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
std.native("jsonToString")({})
|
1
testdata/native3.golden
vendored
Normal file
1
testdata/native3.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
"{\"x\":{\"y\":\"z\"},\"xx\":42}"
|
1
testdata/native3.jsonnet
vendored
Normal file
1
testdata/native3.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
std.native("jsonToString")({"x": {"y": "z"}, "xx": 42})
|
15
testdata/native4.golden
vendored
Normal file
15
testdata/native4.golden
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
RUNTIME ERROR: xxx
|
||||
-------------------------------------------------
|
||||
testdata/native4:1:28-38 thunk from <$>
|
||||
|
||||
std.native("jsonToString")(error "xxx")
|
||||
|
||||
-------------------------------------------------
|
||||
testdata/native4:1:1-40 $
|
||||
|
||||
std.native("jsonToString")(error "xxx")
|
||||
|
||||
-------------------------------------------------
|
||||
During evaluation
|
||||
|
||||
|
1
testdata/native4.jsonnet
vendored
Normal file
1
testdata/native4.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
std.native("jsonToString")(error "xxx")
|
10
testdata/native5.golden
vendored
Normal file
10
testdata/native5.golden
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
RUNTIME ERROR: Couldn't manifest function in JSON output.
|
||||
-------------------------------------------------
|
||||
testdata/native5:1:1-42 $
|
||||
|
||||
std.native("jsonToString")(function() 42)
|
||||
|
||||
-------------------------------------------------
|
||||
During evaluation
|
||||
|
||||
|
1
testdata/native5.jsonnet
vendored
Normal file
1
testdata/native5.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
std.native("jsonToString")(function() 42)
|
8
testdata/native_nonexistent.golden
vendored
Normal file
8
testdata/native_nonexistent.golden
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
RUNTIME ERROR: Unrecognized native function name: blah
|
||||
-------------------------------------------------
|
||||
<builtin> builtin function <native>
|
||||
|
||||
-------------------------------------------------
|
||||
During evaluation
|
||||
|
||||
|
1
testdata/native_nonexistent.jsonnet
vendored
Normal file
1
testdata/native_nonexistent.jsonnet
vendored
Normal file
@ -0,0 +1 @@
|
||||
std.native("blah")
|
31
thunks.go
31
thunks.go
@ -287,6 +287,37 @@ func makeClosure(env environment, function *ast.Function) *closure {
|
||||
}
|
||||
}
|
||||
|
||||
type nativeFunction struct {
|
||||
f func([]interface{}) (interface{}, error)
|
||||
params ast.Identifiers
|
||||
name string
|
||||
}
|
||||
|
||||
func (native *nativeFunction) EvalCall(arguments callArguments, e *evaluator) (value, error) {
|
||||
flatArgs := flattenArgs(arguments, native.Parameters())
|
||||
nativeArgs := make([]interface{}, 0, len(flatArgs))
|
||||
for _, arg := range flatArgs {
|
||||
v, err := e.evaluate(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
json, err := e.i.manifestJSON(e.trace, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nativeArgs = append(nativeArgs, json)
|
||||
}
|
||||
resultJSON, err := native.f(nativeArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonToValue(e, resultJSON)
|
||||
}
|
||||
|
||||
func (native *nativeFunction) Parameters() Parameters {
|
||||
return Parameters{required: native.params}
|
||||
}
|
||||
|
||||
// partialPotentialValue
|
||||
// -------------------------------------
|
||||
|
||||
|
31
vm.go
31
vm.go
@ -31,12 +31,13 @@ import (
|
||||
// VM is the core interpreter and is the touchpoint used to parse and execute
|
||||
// Jsonnet.
|
||||
type VM struct {
|
||||
MaxStack int
|
||||
MaxTrace int // The number of lines of stack trace to display (0 for all of them).
|
||||
ext vmExtMap
|
||||
tla vmExtMap
|
||||
importer Importer
|
||||
ef ErrorFormatter
|
||||
MaxStack int
|
||||
MaxTrace int // The number of lines of stack trace to display (0 for all of them).
|
||||
ext vmExtMap
|
||||
tla vmExtMap
|
||||
nativeFuncs map[string]*nativeFunction
|
||||
importer Importer
|
||||
ef ErrorFormatter
|
||||
}
|
||||
|
||||
// External variable or top level argument provided before execution
|
||||
@ -53,11 +54,12 @@ type vmExtMap map[string]vmExt
|
||||
// MakeVM creates a new VM with default parameters.
|
||||
func MakeVM() *VM {
|
||||
return &VM{
|
||||
MaxStack: 500,
|
||||
ext: make(vmExtMap),
|
||||
tla: make(vmExtMap),
|
||||
ef: ErrorFormatter{pretty: true, colorful: true, MaxStackTraceSize: 20},
|
||||
importer: &FileImporter{},
|
||||
MaxStack: 500,
|
||||
ext: make(vmExtMap),
|
||||
tla: make(vmExtMap),
|
||||
nativeFuncs: make(map[string]*nativeFunction),
|
||||
ef: ErrorFormatter{pretty: true, colorful: true, MaxStackTraceSize: 20},
|
||||
importer: &FileImporter{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,13 +98,18 @@ func (vm *VM) evaluateSnippet(filename string, snippet string) (output string, e
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output, err = evaluate(node, vm.ext, vm.tla, vm.MaxStack, vm.importer)
|
||||
output, err = evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// NativeFunction registers a native function
|
||||
func (vm *VM) NativeFunction(f *nativeFunction) {
|
||||
vm.nativeFuncs[f.name] = f
|
||||
}
|
||||
|
||||
// EvaluateSnippet evaluates a string containing Jsonnet code, return a JSON
|
||||
// string.
|
||||
//
|
||||
|
Loading…
x
Reference in New Issue
Block a user