blob: 886f7dcbaa1e3d718fe6b6c9c741a459ae0da0f5 [file] [log] [blame]
package pyl
/*
Package pyl provides utilities for reading and writing .pyl files. These
contain a single Python literal expression which is typically made up of nested
dicts and/or lists. We define a path format which determines how we traverse
the literal in order to find where a particular revision is specified.
For example, the path "key1.key2.id=my-dependency-id.revision" would
traverse the following literal to find the revision ID "12345":
{
"key1": {
"key2": [
{
"id": "my-dependency-id",
"revision": "12345",
},
],
},
}
Path elements indicate either dictionary keys or selectors which match a
property of a list element.
*/
import (
"reflect"
"strings"
"github.com/go-python/gpython/ast"
"github.com/go-python/gpython/parser"
"go.skia.org/infra/go/skerr"
)
// pathElem describes one element in a path into a .pyl file.
type pathElem struct {
Key string
ValueMatch string
}
// parsePath parses a path to identify a particular element in the form of,
// "key1.key2.key3=value3.key4"
func parsePath(path string) ([]*pathElem, error) {
pathSplit := strings.Split(path, ".")
rv := make([]*pathElem, 0, len(pathSplit))
for _, elem := range pathSplit {
equalSplit := strings.Split(elem, "=")
pathElem := &pathElem{
Key: equalSplit[0],
}
if len(equalSplit) > 1 {
pathElem.ValueMatch = equalSplit[1]
}
rv = append(rv, pathElem)
}
return rv, nil
}
// Get retrieves the value at the given path.
func Get(contents, path string) (string, error) {
// Parse the path.
pathElems, err := parsePath(path)
if err != nil {
return "", skerr.Wrap(err)
}
// Parse the .pyl file as a Python expression.
parsed, err := parser.ParseString(contents, "eval")
if err != nil {
return "", skerr.Wrap(err)
}
expr, ok := parsed.(*ast.Expression)
if !ok {
return "", skerr.Fmt("Parsed value is not an expression: %s", reflect.TypeOf(parsed))
}
elem, _, err := getElem(expr.Body, pathElems)
if err != nil {
return "", skerr.Wrapf(err, "failed to parse expression")
}
return elem, nil
}
// Set sets the value at the given path.
func Set(contents, path, value string) (string, error) {
// Parse the path.
pathElems, err := parsePath(path)
if err != nil {
return "", skerr.Wrap(err)
}
// Parse the .pyl file as a Python expression.
parsed, err := parser.ParseString(contents, "eval")
if err != nil {
return "", skerr.Wrap(err)
}
expr, ok := parsed.(*ast.Expression)
if !ok {
return "", skerr.Fmt("Parsed value is not an expression: %s", reflect.TypeOf(parsed))
}
elem, pos, err := getElem(expr.Body, pathElems)
if err != nil {
return "", skerr.Wrapf(err, "failed to parse expression")
}
contentsLines := strings.Split(contents, "\n")
lineIdx := pos.Lineno - 1 // Lineno starts at 1.
line := contentsLines[lineIdx]
newLine := line[:pos.ColOffset] + strings.Replace(line[pos.ColOffset:], elem, value, 1)
contentsLines[lineIdx] = newLine
return strings.Join(contentsLines, "\n"), nil
}
// getElem retrieves the string at the given path as well as its position within
// the source file.
func getElem(expr ast.Expr, path []*pathElem) (string, ast.Pos, error) {
if len(path) == 0 {
if str, ok := expr.(*ast.Str); ok {
return string(str.S), str.Pos, nil
}
return "", ast.Pos{}, skerr.Fmt("expected string at end of path but found %s", expr.Type())
}
pathEle := path[0]
if dict, ok := expr.(*ast.Dict); ok {
// Note: we're just assuming the use of string keys for dictionaries.
matchIndex := -1
for idx, keyExpr := range dict.Keys {
keyStr, ok := keyExpr.(*ast.Str)
if !ok {
return "", ast.Pos{}, skerr.Fmt("expected string key for dictionary but found %q", keyExpr.Type())
}
key := string(keyStr.S)
if key == pathEle.Key {
matchIndex = idx
}
}
if matchIndex == -1 {
return "", ast.Pos{}, skerr.Fmt("no matching entry for key %q", pathEle.Key)
}
valExpr := dict.Values[matchIndex]
return getElem(valExpr, path[1:])
} else if list, ok := expr.(*ast.List); ok {
for _, elemExpr := range list.Elts {
dict, ok := elemExpr.(*ast.Dict)
if !ok {
return "", ast.Pos{}, skerr.Fmt("expected list of dicts but found %v", elemExpr)
}
matchIndex := -1
for idx, keyExpr := range dict.Keys {
keyStr, ok := keyExpr.(*ast.Str)
if !ok {
return "", ast.Pos{}, skerr.Fmt("expected string key for dictionary but found %q", keyExpr.Type())
}
key := string(keyStr.S)
if key == pathEle.Key {
matchIndex = idx
}
}
if matchIndex == -1 {
return "", ast.Pos{}, skerr.Fmt("no matching entry for key %q", pathEle.Key)
}
valExpr := dict.Values[matchIndex]
if strExp, ok := valExpr.(*ast.Str); ok && string(strExp.S) == pathEle.ValueMatch {
return getElem(elemExpr, path[1:])
}
}
return "", ast.Pos{}, skerr.Fmt("no matching entry with key %s == %s in %+v", pathEle.Key, pathEle.ValueMatch, list)
} else {
return "", ast.Pos{}, skerr.Fmt("expected list or dictionary before end of path but found %+v", expr)
}
}