-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Marko Mikulicic
committed
Jul 27, 2020
1 parent
6905dcc
commit de6ee0a
Showing
6 changed files
with
281 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
// Copyright 2020 VMware, Inc. | ||
// SPDX-License-Identifier: BSD-2-Clause | ||
|
||
package lensed | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/go-openapi/jsonpointer" | ||
"github.com/google/go-jsonnet" | ||
"github.com/google/go-jsonnet/ast" | ||
"github.com/vmware-labs/go-yaml-edit/splice" | ||
"golang.org/x/text/transform" | ||
) | ||
|
||
// Jsonnet implements the "jsonnet" lens. | ||
// | ||
// The jsonnet lens implementation is still in its early stages, it only supports: | ||
// 1. double quoted string scalars | ||
// 2. single level ~{"foo":"bar", "baz":"quz"} matchers | ||
type Jsonnet struct{} | ||
|
||
// Apply implements the Lens interface. | ||
func (Jsonnet) Apply(src []byte, vals []Setter) ([]byte, error) { | ||
var ops []splice.Op | ||
|
||
root, err := jsonnet.SnippetToAST("-", string(src)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
for _, v := range vals { | ||
p, err := jsonpointer.New(v.Pointer) | ||
if err != nil { | ||
return nil, err | ||
} | ||
path := p.DecodedTokens() | ||
|
||
n, err := findJsonnetNode(root, path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
start, err := lineColToPos(src, n.Loc().Begin.Line, n.Loc().Begin.Column+1) | ||
if err != nil { | ||
return nil, err | ||
} | ||
end, err := lineColToPos(src, n.Loc().End.Line, n.Loc().End.Column+1) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var oldval []byte | ||
switch n := n.(type) { | ||
case *ast.LiteralString: | ||
oldval = []byte(n.Value) | ||
default: | ||
return nil, fmt.Errorf("unhandled node type %T", n) | ||
} | ||
|
||
newval, err := v.Value.Transform(oldval) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
ops = append(ops, splice.Span(start, end).With(fmt.Sprintf("%q", string(newval)))) | ||
} | ||
b, _, err := transform.Bytes(splice.T(ops...), src) | ||
return b, err | ||
} | ||
|
||
func findJsonnetNode(root ast.Node, path []string) (ast.Node, error) { | ||
if len(path) == 0 { | ||
return root, nil | ||
} | ||
p := path[0] | ||
|
||
switch n := root.(type) { | ||
case *ast.DesugaredObject: | ||
for _, f := range n.Fields { | ||
if k, ok := f.Name.(*ast.LiteralString); !ok { | ||
continue | ||
} else if p == k.Value { | ||
return findJsonnetNode(f.Body, path[1:]) | ||
} | ||
} | ||
case *ast.Array: | ||
var exprs []ast.Node | ||
for _, e := range n.Elements { | ||
exprs = append(exprs, e.Expr) | ||
} | ||
|
||
e, err := matchJsonnetArrayItem(p, exprs) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return findJsonnetNode(e, path[1:]) | ||
case *ast.Local: | ||
return findJsonnetNode(n.Body, path) | ||
default: | ||
return nil, fmt.Errorf("unsupported jsonnet node type: %T", n) | ||
} | ||
|
||
return nil, fmt.Errorf("cannot find field %q in object", p) | ||
} | ||
|
||
func lineColToPos(src []byte, line, column int) (int, error) { | ||
l, c := 1, 1 | ||
for i, r := range string(src) { | ||
c++ | ||
if r == '\n' { | ||
l++ | ||
c = 1 | ||
} | ||
if l == line && c == column { | ||
return i, nil | ||
} | ||
} | ||
return 0, io.EOF | ||
} | ||
|
||
func matchJsonnetArrayItem(p string, exprs []ast.Node) (ast.Node, error) { | ||
if strings.HasPrefix(p, "~{") { | ||
var m map[string]string | ||
if err := json.Unmarshal([]byte(p[1:]), &m); err != nil { | ||
return nil, err | ||
} | ||
nodes, err := filterJsonnetArrayItems(exprs, isTreeSubsetPred(m)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if got, want := len(nodes), 1; got != want { | ||
return nil, fmt.Errorf("bad number of subtree matches: got=%d, want=%d", got, want) | ||
} | ||
return nodes[0], nil | ||
} | ||
i, err := strconv.Atoi(p) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return exprs[i], nil | ||
} | ||
|
||
type jsonnetNodePredicate func(ast.Node) bool | ||
|
||
func isTreeSubsetPred(a map[string]string) jsonnetNodePredicate { | ||
return func(b ast.Node) bool { | ||
return isTreeSubset(a, b) | ||
} | ||
} | ||
|
||
func isTreeSubset(a map[string]string, b ast.Node) bool { | ||
for k, v := range a { | ||
if !jsonnetObjectHasField(b, k, v) { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
func jsonnetObjectHasField(b ast.Node, name, value string) bool { | ||
if o, ok := b.(*ast.DesugaredObject); ok { | ||
for _, f := range o.Fields { | ||
if n, ok := f.Name.(*ast.LiteralString); ok && n.Value == name { | ||
v, ok := f.Body.(*ast.LiteralString) | ||
return ok && v.Value == value | ||
} | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func filterJsonnetArrayItems(nodes []ast.Node, pred jsonnetNodePredicate) ([]ast.Node, error) { | ||
var res []ast.Node | ||
for _, n := range nodes { | ||
if pred(n) { | ||
res = append(res, n) | ||
} | ||
} | ||
return res, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
// Copyright 2020 VMware, Inc. | ||
// SPDX-License-Identifier: BSD-2-Clause | ||
|
||
package lensed | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
func TestJsonnet(t *testing.T) { | ||
testCases := []struct { | ||
src string | ||
t []Mapping | ||
want string | ||
}{ | ||
{ | ||
`{foo:{bar:"xyz"}}`, | ||
[]Mapping{ | ||
{"~(jsonnet)/foo/bar", "abc"}, | ||
}, | ||
`{foo:{bar:"abc"}}`, | ||
}, | ||
{ | ||
`{foo: | ||
{bar:"xyz"}}`, | ||
[]Mapping{ | ||
{"~(jsonnet)/foo/bar", "abc"}, | ||
}, | ||
`{foo: | ||
{bar:"abc"}}`, | ||
}, | ||
{ | ||
`{foo:["xyz"]}`, | ||
[]Mapping{ | ||
{"~(jsonnet)/foo/0", "abc"}, | ||
}, | ||
`{foo:["abc"]}`, | ||
}, | ||
{ | ||
`{foo:[{bar:"xyz"}]}`, | ||
[]Mapping{ | ||
{"~(jsonnet)/foo/0/bar", "abc"}, | ||
}, | ||
`{foo:[{bar:"abc"}]}`, | ||
}, | ||
{ | ||
`{foo:[{name:"wrong",bar:"ppp"},{name:"right",bar:"xyz"}]}`, | ||
[]Mapping{ | ||
{`~(jsonnet)/foo/~{"name":"right"}/bar`, "abc"}, | ||
}, | ||
`{foo:[{name:"wrong",bar:"ppp"},{name:"right",bar:"abc"}]}`, | ||
}, | ||
{ | ||
`{local a="b",foo:{bar:"xyz"}}`, | ||
[]Mapping{ | ||
{"~(jsonnet)/foo/bar", "abc"}, | ||
}, | ||
`{local a="b",foo:{bar:"abc"}}`, | ||
}, | ||
{ | ||
`local a=1; {foo:{bar:"xyz"}}`, | ||
[]Mapping{ | ||
{"~(jsonnet)/foo/bar", "abc"}, | ||
}, | ||
`local a=1; {foo:{bar:"abc"}}`, | ||
}, | ||
} | ||
|
||
for i, tc := range testCases { | ||
t.Run(fmt.Sprint(i), func(t *testing.T) { | ||
got, err := Default.Apply([]byte(tc.src), tc.t) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if got, want := string(got), tc.want; got != want { | ||
t.Errorf("got: %q, want: %q", got, want) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters