github.com/kubevela/workflow@v0.6.0/pkg/cue/model/sets/operation.go (about)

     1  /*
     2  Copyright 2022 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package sets
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  
    23  	"cuelang.org/go/cue"
    24  	"cuelang.org/go/cue/ast"
    25  	"cuelang.org/go/cue/cuecontext"
    26  	"cuelang.org/go/cue/parser"
    27  	jsonpatch "github.com/evanphx/json-patch"
    28  	"github.com/pkg/errors"
    29  )
    30  
    31  const (
    32  	// TagPatchKey specify the primary key of the list items
    33  	TagPatchKey = "patchKey"
    34  	// TagPatchStrategy specify a strategy of the strategic merge patch
    35  	TagPatchStrategy = "patchStrategy"
    36  
    37  	// StrategyRetainKeys notes on the strategic merge patch using the retainKeys strategy
    38  	StrategyRetainKeys = "retainKeys"
    39  	// StrategyReplace notes on the strategic merge patch will allow replacing list
    40  	StrategyReplace = "replace"
    41  	// StrategyJSONPatch notes on the strategic merge patch will follow the RFC 6902 to run JsonPatch
    42  	StrategyJSONPatch = "jsonPatch"
    43  	// StrategyJSONMergePatch notes on the strategic merge patch will follow the RFC 7396 to run JsonMergePatch
    44  	StrategyJSONMergePatch = "jsonMergePatch"
    45  )
    46  
    47  var (
    48  	notFoundErr = errors.Errorf("not found")
    49  )
    50  
    51  // UnifyParams params for unify
    52  type UnifyParams struct {
    53  	PatchStrategy string
    54  }
    55  
    56  // UnifyOption defines the option for unify
    57  type UnifyOption interface {
    58  	ApplyToOption(params *UnifyParams)
    59  }
    60  
    61  // UnifyByJSONPatch unify by json patch following RFC 6902
    62  type UnifyByJSONPatch struct{}
    63  
    64  // ApplyToOption apply to option
    65  func (op UnifyByJSONPatch) ApplyToOption(params *UnifyParams) {
    66  	params.PatchStrategy = StrategyJSONPatch
    67  }
    68  
    69  // UnifyByJSONMergePatch unify by json patch following RFC 7396
    70  type UnifyByJSONMergePatch struct{}
    71  
    72  // ApplyToOption apply to option
    73  func (op UnifyByJSONMergePatch) ApplyToOption(params *UnifyParams) {
    74  	params.PatchStrategy = StrategyJSONMergePatch
    75  }
    76  
    77  func newUnifyParams(options ...UnifyOption) *UnifyParams {
    78  	params := &UnifyParams{}
    79  	for _, op := range options {
    80  		op.ApplyToOption(params)
    81  	}
    82  	return params
    83  }
    84  
    85  // CreateUnifyOptionsForPatcher create unify options for patcher
    86  func CreateUnifyOptionsForPatcher(patcher cue.Value) (options []UnifyOption) {
    87  	if IsJSONPatch(patcher) {
    88  		options = append(options, UnifyByJSONPatch{})
    89  	} else if IsJSONMergePatch(patcher) {
    90  		options = append(options, UnifyByJSONMergePatch{})
    91  	}
    92  	return
    93  }
    94  
    95  type interceptor func(baseNode ast.Node, patchNode ast.Node) error
    96  
    97  func listMergeProcess(field *ast.Field, key string, baseList, patchList *ast.ListLit) {
    98  	kmaps := map[string]ast.Expr{}
    99  	nElts := []ast.Expr{}
   100  	keys := strings.Split(key, ",")
   101  	for _, key := range keys {
   102  		foundPatch := false
   103  		for i, elt := range patchList.Elts {
   104  			if _, ok := elt.(*ast.Ellipsis); ok {
   105  				continue
   106  			}
   107  			nodev, err := lookUp(elt, strings.Split(key, ".")...)
   108  			if err != nil {
   109  				continue
   110  			}
   111  			foundPatch = true
   112  			blit, ok := nodev.(*ast.BasicLit)
   113  			if !ok {
   114  				return
   115  			}
   116  			kmaps[fmt.Sprintf(key, blit.Value)] = patchList.Elts[i]
   117  		}
   118  		if !foundPatch {
   119  			if len(patchList.Elts) == 0 {
   120  				continue
   121  			}
   122  			return
   123  		}
   124  
   125  		hasStrategyRetainKeys := isStrategyRetainKeys(field)
   126  
   127  		for i, elt := range baseList.Elts {
   128  			if _, ok := elt.(*ast.Ellipsis); ok {
   129  				continue
   130  			}
   131  
   132  			nodev, err := lookUp(elt, strings.Split(key, ".")...)
   133  			if err != nil {
   134  				continue
   135  			}
   136  			blit, ok := nodev.(*ast.BasicLit)
   137  			if !ok {
   138  				return
   139  			}
   140  
   141  			k := fmt.Sprintf(key, blit.Value)
   142  			if v, ok := kmaps[k]; ok {
   143  				if hasStrategyRetainKeys {
   144  					baseList.Elts[i] = ast.NewStruct()
   145  				}
   146  				nElts = append(nElts, v)
   147  				delete(kmaps, k)
   148  			} else {
   149  				nElts = append(nElts, ast.NewStruct())
   150  			}
   151  
   152  		}
   153  	}
   154  	for _, elt := range patchList.Elts {
   155  		for _, v := range kmaps {
   156  			if elt == v {
   157  				nElts = append(nElts, v)
   158  				break
   159  			}
   160  		}
   161  	}
   162  
   163  	nElts = append(nElts, &ast.Ellipsis{})
   164  	patchList.Elts = nElts
   165  }
   166  
   167  func strategyPatchHandle() interceptor {
   168  	return func(baseNode ast.Node, patchNode ast.Node) error {
   169  		walker := newWalker(func(node ast.Node, ctx walkCtx) {
   170  			field, ok := node.(*ast.Field)
   171  			if !ok {
   172  				return
   173  			}
   174  
   175  			value := peelCloseExpr(field.Value)
   176  
   177  			switch val := value.(type) {
   178  			case *ast.ListLit:
   179  				key := ctx.Tags()[TagPatchKey]
   180  				patchStrategy := ""
   181  				tags := findCommentTag(field.Comments())
   182  				for tk, tv := range tags {
   183  					if tk == TagPatchKey {
   184  						key = tv
   185  					}
   186  					if tk == TagPatchStrategy {
   187  						patchStrategy = tv
   188  					}
   189  				}
   190  
   191  				paths := append(ctx.Pos(), LabelStr(field.Label))
   192  				baseSubNode, err := lookUp(baseNode, paths...)
   193  				if err != nil {
   194  					if errors.Is(err, notFoundErr) {
   195  						return
   196  					}
   197  					baseSubNode = ast.NewList()
   198  				}
   199  				baselist, ok := baseSubNode.(*ast.ListLit)
   200  				if !ok {
   201  					return
   202  				}
   203  				if patchStrategy == StrategyReplace {
   204  					baselist.Elts = val.Elts
   205  				} else if key != "" {
   206  					listMergeProcess(field, key, baselist, val)
   207  				}
   208  
   209  			default:
   210  				if !isStrategyRetainKeys(field) {
   211  					return
   212  				}
   213  
   214  				srcNode, _ := lookUp(baseNode, ctx.Pos()...)
   215  				if srcNode != nil {
   216  					switch v := srcNode.(type) {
   217  					case *ast.StructLit:
   218  						for _, elt := range v.Elts {
   219  							if fe, ok := elt.(*ast.Field); ok &&
   220  								LabelStr(fe.Label) == LabelStr(field.Label) {
   221  								fe.Value = field.Value
   222  							}
   223  						}
   224  					case *ast.File: // For the top level element
   225  						for _, decl := range v.Decls {
   226  							if fe, ok := decl.(*ast.Field); ok &&
   227  								LabelStr(fe.Label) == LabelStr(field.Label) {
   228  								fe.Value = field.Value
   229  							}
   230  						}
   231  					}
   232  				}
   233  			}
   234  		})
   235  		walker.walk(patchNode)
   236  		return nil
   237  	}
   238  }
   239  
   240  func isStrategyRetainKeys(node *ast.Field) bool {
   241  	tags := findCommentTag(node.Comments())
   242  	for tk, tv := range tags {
   243  		if tk == TagPatchStrategy && tv == StrategyRetainKeys {
   244  			return true
   245  		}
   246  	}
   247  	return false
   248  }
   249  
   250  // IsJSONMergePatch check if patcher is json merge patch
   251  func IsJSONMergePatch(patcher cue.Value) bool {
   252  	tags := findCommentTag(patcher.Doc())
   253  	return tags[TagPatchStrategy] == StrategyJSONMergePatch
   254  }
   255  
   256  // IsJSONPatch check if patcher is json patch
   257  func IsJSONPatch(patcher cue.Value) bool {
   258  	tags := findCommentTag(patcher.Doc())
   259  	return tags[TagPatchStrategy] == StrategyJSONPatch
   260  }
   261  
   262  // StrategyUnify unify the objects by the strategy
   263  func StrategyUnify(base, patch cue.Value, options ...UnifyOption) (ret cue.Value, err error) {
   264  	params := newUnifyParams(options...)
   265  	var patchOpts []interceptor
   266  	if params.PatchStrategy == StrategyJSONMergePatch || params.PatchStrategy == StrategyJSONPatch {
   267  		_, err := OpenBaiscLit(base)
   268  		if err != nil {
   269  			return base, err
   270  		}
   271  	} else {
   272  		patchOpts = []interceptor{strategyPatchHandle()}
   273  	}
   274  	return strategyUnify(base, patch, params, patchOpts...)
   275  }
   276  
   277  // nolint:staticcheck
   278  func strategyUnify(base cue.Value, patch cue.Value, params *UnifyParams, patchOpts ...interceptor) (val cue.Value, err error) {
   279  	if params.PatchStrategy == StrategyJSONMergePatch {
   280  		return jsonMergePatch(base, patch)
   281  	} else if params.PatchStrategy == StrategyJSONPatch {
   282  		return jsonPatch(base, patch.LookupPath(cue.ParsePath("operations")))
   283  	}
   284  	openBase, err := OpenListLit(base)
   285  	if err != nil {
   286  		return cue.Value{}, errors.Wrapf(err, "failed to open list it for merge")
   287  	}
   288  	patchFile, err := ToFile(patch.Syntax(cue.Docs(true), cue.ResolveReferences(true)))
   289  	if err != nil {
   290  		return cue.Value{}, err
   291  	}
   292  	for _, option := range patchOpts {
   293  		if err := option(openBase, patchFile); err != nil {
   294  			return cue.Value{}, errors.WithMessage(err, "process patchOption")
   295  		}
   296  	}
   297  
   298  	baseInst := cuecontext.New().BuildFile(openBase)
   299  	patchInst := cuecontext.New().BuildFile(patchFile)
   300  
   301  	ret := baseInst.Unify(patchInst)
   302  
   303  	_, err = toString(ret, removeTmpVar)
   304  	if err != nil {
   305  		return ret, errors.WithMessage(err, " format result toString")
   306  	}
   307  
   308  	if err := ret.Err(); err != nil {
   309  		return ret, errors.WithMessage(err, "result check err")
   310  	}
   311  
   312  	if err := ret.Validate(cue.All()); err != nil {
   313  		return ret, errors.WithMessage(err, "result validate")
   314  	}
   315  
   316  	return ret, nil
   317  }
   318  
   319  func findCommentTag(commentGroup []*ast.CommentGroup) map[string]string {
   320  	marker := "+"
   321  	kval := map[string]string{}
   322  	for _, group := range commentGroup {
   323  		for _, lineT := range group.List {
   324  			line := lineT.Text
   325  			line = strings.TrimPrefix(line, "//")
   326  			line = strings.TrimSpace(line)
   327  			if len(line) == 0 {
   328  				continue
   329  			}
   330  			if !strings.HasPrefix(line, marker) {
   331  				continue
   332  			}
   333  			kv := strings.SplitN(line[len(marker):], "=", 2)
   334  			if len(kv) == 2 {
   335  				val := strings.TrimSpace(kv[1])
   336  				if len(strings.Fields(val)) > 1 {
   337  					continue
   338  				}
   339  				kval[strings.TrimSpace(kv[0])] = val
   340  			}
   341  		}
   342  	}
   343  	return kval
   344  }
   345  
   346  func jsonMergePatch(base cue.Value, patch cue.Value) (cue.Value, error) {
   347  	ctx := cuecontext.New()
   348  	baseJSON, err := base.MarshalJSON()
   349  	if err != nil {
   350  		return cue.Value{}, errors.Wrapf(err, "failed to marshal base value")
   351  	}
   352  	patchJSON, err := patch.MarshalJSON()
   353  	if err != nil {
   354  		return cue.Value{}, errors.Wrapf(err, "failed to marshal patch value")
   355  	}
   356  	merged, err := jsonpatch.MergePatch(baseJSON, patchJSON)
   357  	if err != nil {
   358  		return cue.Value{}, errors.Wrapf(err, "failed to merge base value and patch value by JsonMergePatch")
   359  	}
   360  	output, err := openJSON(string(merged))
   361  	if err != nil {
   362  		return cue.Value{}, errors.Wrapf(err, "failed to parse open basic lit for merged result")
   363  	}
   364  	return ctx.BuildFile(output), nil
   365  }
   366  
   367  func jsonPatch(base cue.Value, patch cue.Value) (cue.Value, error) {
   368  	ctx := cuecontext.New()
   369  	baseJSON, err := base.MarshalJSON()
   370  	if err != nil {
   371  		return cue.Value{}, errors.Wrapf(err, "failed to marshal base value")
   372  	}
   373  	patchJSON, err := patch.MarshalJSON()
   374  	if err != nil {
   375  		return cue.Value{}, errors.Wrapf(err, "failed to marshal patch value")
   376  	}
   377  	decodedPatch, err := jsonpatch.DecodePatch(patchJSON)
   378  	if err != nil {
   379  		return cue.Value{}, errors.Wrapf(err, "failed to decode patch")
   380  	}
   381  
   382  	merged, err := decodedPatch.Apply(baseJSON)
   383  	if err != nil {
   384  		return cue.Value{}, errors.Wrapf(err, "failed to apply json patch")
   385  	}
   386  	output, err := openJSON(string(merged))
   387  	if err != nil {
   388  		return cue.Value{}, errors.Wrapf(err, "failed to parse open basic lit for merged result")
   389  	}
   390  	return ctx.BuildFile(output), nil
   391  }
   392  
   393  func isEllipsis(elt ast.Node) bool {
   394  	_, ok := elt.(*ast.Ellipsis)
   395  	return ok
   396  }
   397  
   398  func openJSON(data string) (*ast.File, error) {
   399  	f, err := parser.ParseFile("-", data, parser.ParseComments)
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  	ast.Walk(f, func(node ast.Node) bool {
   404  		field, ok := node.(*ast.Field)
   405  		if ok {
   406  			v := field.Value
   407  			switch lit := v.(type) {
   408  			case *ast.StructLit:
   409  				if len(lit.Elts) == 0 || !isEllipsis(lit.Elts[len(lit.Elts)-1]) {
   410  					lit.Elts = append(lit.Elts, &ast.Ellipsis{})
   411  				}
   412  			case *ast.ListLit:
   413  				if len(lit.Elts) == 0 || !isEllipsis(lit.Elts[len(lit.Elts)-1]) {
   414  					lit.Elts = append(lit.Elts, &ast.Ellipsis{})
   415  				}
   416  			}
   417  		}
   418  		return true
   419  	}, nil)
   420  	if len(f.Decls) > 0 {
   421  		if emb, ok := f.Decls[0].(*ast.EmbedDecl); ok {
   422  			if s, _ok := emb.Expr.(*ast.StructLit); _ok {
   423  				f.Decls = s.Elts
   424  			}
   425  		}
   426  	}
   427  	return f, nil
   428  }