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 }