Files
kubevela/pkg/cue/model/sets/operation.go
2022-06-22 19:55:58 +08:00

392 lines
10 KiB
Go

/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sets
import (
"fmt"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/parser"
jsonpatch "github.com/evanphx/json-patch"
"github.com/pkg/errors"
)
const (
// TagPatchKey specify the primary key of the list items
TagPatchKey = "patchKey"
// TagPatchStrategy specify a strategy of the strategic merge patch
TagPatchStrategy = "patchStrategy"
// StrategyRetainKeys notes on the strategic merge patch using the retainKeys strategy
StrategyRetainKeys = "retainKeys"
// StrategyReplace notes on the strategic merge patch will allow replacing list
StrategyReplace = "replace"
// StrategyJSONPatch notes on the strategic merge patch will follow the RFC 6902 to run JsonPatch
StrategyJSONPatch = "jsonPatch"
// StrategyJSONMergePatch notes on the strategic merge patch will follow the RFC 7396 to run JsonMergePatch
StrategyJSONMergePatch = "jsonMergePatch"
)
var (
notFoundErr = errors.Errorf("not found")
)
// UnifyParams params for unify
type UnifyParams struct {
PatchStrategy string
}
// UnifyOption defines the option for unify
type UnifyOption interface {
ApplyToOption(params *UnifyParams)
}
// UnifyByJSONPatch unify by json patch following RFC 6902
type UnifyByJSONPatch struct{}
// ApplyToOption apply to option
func (op UnifyByJSONPatch) ApplyToOption(params *UnifyParams) {
params.PatchStrategy = StrategyJSONPatch
}
// UnifyByJSONMergePatch unify by json patch following RFC 7396
type UnifyByJSONMergePatch struct{}
// ApplyToOption apply to option
func (op UnifyByJSONMergePatch) ApplyToOption(params *UnifyParams) {
params.PatchStrategy = StrategyJSONMergePatch
}
func newUnifyParams(options ...UnifyOption) *UnifyParams {
params := &UnifyParams{}
for _, op := range options {
op.ApplyToOption(params)
}
return params
}
// CreateUnifyOptionsForPatcher create unify options for patcher
func CreateUnifyOptionsForPatcher(patcher cue.Value) (options []UnifyOption) {
if IsJSONPatch(patcher) {
options = append(options, UnifyByJSONPatch{})
} else if IsJSONMergePatch(patcher) {
options = append(options, UnifyByJSONMergePatch{})
}
return
}
type interceptor func(baseNode ast.Node, patchNode ast.Node) error
func listMergeProcess(field *ast.Field, key string, baseList, patchList *ast.ListLit) {
kmaps := map[string]ast.Expr{}
nElts := []ast.Expr{}
keys := strings.Split(key, ",")
for _, key := range keys {
foundPatch := false
for i, elt := range patchList.Elts {
if _, ok := elt.(*ast.Ellipsis); ok {
continue
}
nodev, err := lookUp(elt, strings.Split(key, ".")...)
if err != nil {
continue
}
foundPatch = true
blit, ok := nodev.(*ast.BasicLit)
if !ok {
return
}
kmaps[fmt.Sprintf(key, blit.Value)] = patchList.Elts[i]
}
if !foundPatch {
return
}
hasStrategyRetainKeys := isStrategyRetainKeys(field)
for i, elt := range baseList.Elts {
if _, ok := elt.(*ast.Ellipsis); ok {
continue
}
nodev, err := lookUp(elt, strings.Split(key, ".")...)
if err != nil {
continue
}
blit, ok := nodev.(*ast.BasicLit)
if !ok {
return
}
k := fmt.Sprintf(key, blit.Value)
if v, ok := kmaps[k]; ok {
if hasStrategyRetainKeys {
baseList.Elts[i] = ast.NewStruct()
}
nElts = append(nElts, v)
delete(kmaps, k)
} else {
nElts = append(nElts, ast.NewStruct())
}
}
}
for _, elt := range patchList.Elts {
for _, v := range kmaps {
if elt == v {
nElts = append(nElts, v)
break
}
}
}
nElts = append(nElts, &ast.Ellipsis{})
patchList.Elts = nElts
}
func strategyPatchHandle() interceptor {
return func(baseNode ast.Node, patchNode ast.Node) error {
walker := newWalker(func(node ast.Node, ctx walkCtx) {
field, ok := node.(*ast.Field)
if !ok {
return
}
value := peelCloseExpr(field.Value)
switch val := value.(type) {
case *ast.ListLit:
key := ctx.Tags()[TagPatchKey]
patchStrategy := ""
tags := findCommentTag(field.Comments())
for tk, tv := range tags {
if tk == TagPatchKey {
key = tv
}
if tk == TagPatchStrategy {
patchStrategy = tv
}
}
paths := append(ctx.Pos(), labelStr(field.Label))
baseSubNode, err := lookUp(baseNode, paths...)
if err != nil {
return
}
baselist, ok := baseSubNode.(*ast.ListLit)
if !ok {
return
}
if patchStrategy == StrategyReplace {
baselist.Elts = val.Elts
} else if key != "" {
listMergeProcess(field, key, baselist, val)
}
default:
if !isStrategyRetainKeys(field) {
return
}
srcNode, _ := lookUp(baseNode, ctx.Pos()...)
if srcNode != nil {
switch v := srcNode.(type) {
case *ast.StructLit:
for _, elt := range v.Elts {
if fe, ok := elt.(*ast.Field); ok &&
labelStr(fe.Label) == labelStr(field.Label) {
fe.Value = field.Value
}
}
case *ast.File: // For the top level element
for _, decl := range v.Decls {
if fe, ok := decl.(*ast.Field); ok &&
labelStr(fe.Label) == labelStr(field.Label) {
fe.Value = field.Value
}
}
}
}
}
})
walker.walk(patchNode)
return nil
}
}
func isStrategyRetainKeys(node *ast.Field) bool {
tags := findCommentTag(node.Comments())
for tk, tv := range tags {
if tk == TagPatchStrategy && tv == StrategyRetainKeys {
return true
}
}
return false
}
// IsJSONMergePatch check if patcher is json merge patch
func IsJSONMergePatch(patcher cue.Value) bool {
tags := findCommentTag(patcher.Doc())
return tags[TagPatchStrategy] == StrategyJSONMergePatch
}
// IsJSONPatch check if patcher is json patch
func IsJSONPatch(patcher cue.Value) bool {
tags := findCommentTag(patcher.Doc())
return tags[TagPatchStrategy] == StrategyJSONPatch
}
// StrategyUnify unify the objects by the strategy
func StrategyUnify(base, patch string, options ...UnifyOption) (ret string, err error) {
params := newUnifyParams(options...)
var patchOpts []interceptor
if params.PatchStrategy == StrategyJSONMergePatch || params.PatchStrategy == StrategyJSONPatch {
base, err = OpenBaiscLit(base)
if err != nil {
return base, err
}
} else {
patchOpts = []interceptor{strategyPatchHandle()}
}
baseFile, err := parser.ParseFile("-", base, parser.ParseComments)
if err != nil {
return "", errors.WithMessage(err, "invalid base cue file")
}
patchFile, err := parser.ParseFile("-", patch, parser.ParseComments)
if err != nil {
return "", errors.WithMessage(err, "invalid patch cue file")
}
return strategyUnify(baseFile, patchFile, params, patchOpts...)
}
func strategyUnify(baseFile *ast.File, patchFile *ast.File, params *UnifyParams, patchOpts ...interceptor) (string, error) {
for _, option := range patchOpts {
if err := option(baseFile, patchFile); err != nil {
return "", errors.WithMessage(err, "process patchOption")
}
}
var r cue.Runtime
baseInst, err := r.CompileFile(baseFile)
if err != nil {
return "", errors.WithMessage(err, "compile base file")
}
patchInst, err := r.CompileFile(patchFile)
if err != nil {
return "", errors.WithMessage(err, "compile patch file")
}
if params.PatchStrategy == StrategyJSONMergePatch {
return jsonMergePatch(baseInst.Value(), patchInst.Value())
} else if params.PatchStrategy == StrategyJSONPatch {
return jsonPatch(baseInst.Value(), patchInst.Lookup("operations"))
}
ret := baseInst.Value().Unify(patchInst.Value())
rv, err := toString(ret, removeTmpVar)
if err != nil {
return rv, errors.WithMessage(err, " format result toString")
}
if err := ret.Err(); err != nil {
return rv, errors.WithMessage(err, "result check err")
}
if err := ret.Validate(cue.All()); err != nil {
return rv, errors.WithMessage(err, "result validate")
}
return rv, nil
}
func findCommentTag(commentGroup []*ast.CommentGroup) map[string]string {
marker := "+"
kval := map[string]string{}
for _, group := range commentGroup {
for _, lineT := range group.List {
line := lineT.Text
line = strings.TrimPrefix(line, "//")
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
if !strings.HasPrefix(line, marker) {
continue
}
kv := strings.SplitN(line[len(marker):], "=", 2)
if len(kv) == 2 {
val := strings.TrimSpace(kv[1])
if len(strings.Fields(val)) > 1 {
continue
}
kval[strings.TrimSpace(kv[0])] = val
}
}
}
return kval
}
func jsonMergePatch(base cue.Value, patch cue.Value) (string, error) {
baseJSON, err := base.MarshalJSON()
if err != nil {
return "", errors.Wrapf(err, "failed to marshal base value")
}
patchJSON, err := patch.MarshalJSON()
if err != nil {
return "", errors.Wrapf(err, "failed to marshal patch value")
}
merged, err := jsonpatch.MergePatch(baseJSON, patchJSON)
if err != nil {
return "", errors.Wrapf(err, "failed to merge base value and patch value by JsonMergePatch")
}
output, err := OpenBaiscLit(string(merged))
if err != nil {
return "", errors.Wrapf(err, "failed to parse open basic lit for merged result")
}
return output, nil
}
func jsonPatch(base cue.Value, patch cue.Value) (string, error) {
baseJSON, err := base.MarshalJSON()
if err != nil {
return "", errors.Wrapf(err, "failed to marshal base value")
}
patchJSON, err := patch.MarshalJSON()
if err != nil {
return "", errors.Wrapf(err, "failed to marshal patch value")
}
decodedPatch, err := jsonpatch.DecodePatch(patchJSON)
if err != nil {
return "", errors.Wrapf(err, "failed to decode patch")
}
merged, err := decodedPatch.Apply(baseJSON)
if err != nil {
return "", errors.Wrapf(err, "failed to apply json patch")
}
output, err := OpenBaiscLit(string(merged))
if err != nil {
return "", errors.Wrapf(err, "failed to parse open basic lit for merged result")
}
return output, nil
}