mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-16 22:53:22 +00:00
Implement Strategic Merge Patch in apiserver
This commit is contained in:
469
pkg/util/strategicpatch/fields.go
Normal file
469
pkg/util/strategicpatch/fields.go
Normal file
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// NOTE: The below is taken from the Go standard library to enable us to find
|
||||
// the field of a struct that a given JSON key maps to.
|
||||
//
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
package strategicpatch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A field represents a single field found in a struct.
|
||||
type field struct {
|
||||
name string
|
||||
nameBytes []byte // []byte(name)
|
||||
equalFold func(s, t []byte) bool // bytes.EqualFold or equivalent
|
||||
|
||||
tag bool
|
||||
index []int
|
||||
typ reflect.Type
|
||||
omitEmpty bool
|
||||
quoted bool
|
||||
}
|
||||
|
||||
func fillField(f field) field {
|
||||
f.nameBytes = []byte(f.name)
|
||||
f.equalFold = foldFunc(f.nameBytes)
|
||||
return f
|
||||
}
|
||||
|
||||
// byName sorts field by name, breaking ties with depth,
|
||||
// then breaking ties with "name came from json tag", then
|
||||
// breaking ties with index sequence.
|
||||
type byName []field
|
||||
|
||||
func (x byName) Len() int { return len(x) }
|
||||
|
||||
func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
|
||||
func (x byName) Less(i, j int) bool {
|
||||
if x[i].name != x[j].name {
|
||||
return x[i].name < x[j].name
|
||||
}
|
||||
if len(x[i].index) != len(x[j].index) {
|
||||
return len(x[i].index) < len(x[j].index)
|
||||
}
|
||||
if x[i].tag != x[j].tag {
|
||||
return x[i].tag
|
||||
}
|
||||
return byIndex(x).Less(i, j)
|
||||
}
|
||||
|
||||
// byIndex sorts field by index sequence.
|
||||
type byIndex []field
|
||||
|
||||
func (x byIndex) Len() int { return len(x) }
|
||||
|
||||
func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
|
||||
func (x byIndex) Less(i, j int) bool {
|
||||
for k, xik := range x[i].index {
|
||||
if k >= len(x[j].index) {
|
||||
return false
|
||||
}
|
||||
if xik != x[j].index[k] {
|
||||
return xik < x[j].index[k]
|
||||
}
|
||||
}
|
||||
return len(x[i].index) < len(x[j].index)
|
||||
}
|
||||
|
||||
// typeFields returns a list of fields that JSON should recognize for the given type.
|
||||
// The algorithm is breadth-first search over the set of structs to include - the top struct
|
||||
// and then any reachable anonymous structs.
|
||||
func typeFields(t reflect.Type) []field {
|
||||
// Anonymous fields to explore at the current level and the next.
|
||||
current := []field{}
|
||||
next := []field{{typ: t}}
|
||||
|
||||
// Count of queued names for current level and the next.
|
||||
count := map[reflect.Type]int{}
|
||||
nextCount := map[reflect.Type]int{}
|
||||
|
||||
// Types already visited at an earlier level.
|
||||
visited := map[reflect.Type]bool{}
|
||||
|
||||
// Fields found.
|
||||
var fields []field
|
||||
|
||||
for len(next) > 0 {
|
||||
current, next = next, current[:0]
|
||||
count, nextCount = nextCount, map[reflect.Type]int{}
|
||||
|
||||
for _, f := range current {
|
||||
if visited[f.typ] {
|
||||
continue
|
||||
}
|
||||
visited[f.typ] = true
|
||||
|
||||
// Scan f.typ for fields to include.
|
||||
for i := 0; i < f.typ.NumField(); i++ {
|
||||
sf := f.typ.Field(i)
|
||||
if sf.PkgPath != "" { // unexported
|
||||
continue
|
||||
}
|
||||
tag := sf.Tag.Get("json")
|
||||
if tag == "-" {
|
||||
continue
|
||||
}
|
||||
name, opts := parseTag(tag)
|
||||
if !isValidTag(name) {
|
||||
name = ""
|
||||
}
|
||||
index := make([]int, len(f.index)+1)
|
||||
copy(index, f.index)
|
||||
index[len(f.index)] = i
|
||||
|
||||
ft := sf.Type
|
||||
if ft.Name() == "" && ft.Kind() == reflect.Ptr {
|
||||
// Follow pointer.
|
||||
ft = ft.Elem()
|
||||
}
|
||||
|
||||
// Record found field and index sequence.
|
||||
if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
|
||||
tagged := name != ""
|
||||
if name == "" {
|
||||
name = sf.Name
|
||||
}
|
||||
fields = append(fields, fillField(field{
|
||||
name: name,
|
||||
tag: tagged,
|
||||
index: index,
|
||||
typ: ft,
|
||||
omitEmpty: opts.Contains("omitempty"),
|
||||
quoted: opts.Contains("string"),
|
||||
}))
|
||||
if count[f.typ] > 1 {
|
||||
// If there were multiple instances, add a second,
|
||||
// so that the annihilation code will see a duplicate.
|
||||
// It only cares about the distinction between 1 or 2,
|
||||
// so don't bother generating any more copies.
|
||||
fields = append(fields, fields[len(fields)-1])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Record new anonymous struct to explore in next round.
|
||||
nextCount[ft]++
|
||||
if nextCount[ft] == 1 {
|
||||
next = append(next, fillField(field{name: ft.Name(), index: index, typ: ft}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byName(fields))
|
||||
|
||||
// Delete all fields that are hidden by the Go rules for embedded fields,
|
||||
// except that fields with JSON tags are promoted.
|
||||
|
||||
// The fields are sorted in primary order of name, secondary order
|
||||
// of field index length. Loop over names; for each name, delete
|
||||
// hidden fields by choosing the one dominant field that survives.
|
||||
out := fields[:0]
|
||||
for advance, i := 0, 0; i < len(fields); i += advance {
|
||||
// One iteration per name.
|
||||
// Find the sequence of fields with the name of this first field.
|
||||
fi := fields[i]
|
||||
name := fi.name
|
||||
for advance = 1; i+advance < len(fields); advance++ {
|
||||
fj := fields[i+advance]
|
||||
if fj.name != name {
|
||||
break
|
||||
}
|
||||
}
|
||||
if advance == 1 { // Only one field with this name
|
||||
out = append(out, fi)
|
||||
continue
|
||||
}
|
||||
dominant, ok := dominantField(fields[i : i+advance])
|
||||
if ok {
|
||||
out = append(out, dominant)
|
||||
}
|
||||
}
|
||||
|
||||
fields = out
|
||||
sort.Sort(byIndex(fields))
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// dominantField looks through the fields, all of which are known to
|
||||
// have the same name, to find the single field that dominates the
|
||||
// others using Go's embedding rules, modified by the presence of
|
||||
// JSON tags. If there are multiple top-level fields, the boolean
|
||||
// will be false: This condition is an error in Go and we skip all
|
||||
// the fields.
|
||||
func dominantField(fields []field) (field, bool) {
|
||||
// The fields are sorted in increasing index-length order. The winner
|
||||
// must therefore be one with the shortest index length. Drop all
|
||||
// longer entries, which is easy: just truncate the slice.
|
||||
length := len(fields[0].index)
|
||||
tagged := -1 // Index of first tagged field.
|
||||
for i, f := range fields {
|
||||
if len(f.index) > length {
|
||||
fields = fields[:i]
|
||||
break
|
||||
}
|
||||
if f.tag {
|
||||
if tagged >= 0 {
|
||||
// Multiple tagged fields at the same level: conflict.
|
||||
// Return no field.
|
||||
return field{}, false
|
||||
}
|
||||
tagged = i
|
||||
}
|
||||
}
|
||||
if tagged >= 0 {
|
||||
return fields[tagged], true
|
||||
}
|
||||
// All remaining fields have the same length. If there's more than one,
|
||||
// we have a conflict (two fields named "X" at the same level) and we
|
||||
// return no field.
|
||||
if len(fields) > 1 {
|
||||
return field{}, false
|
||||
}
|
||||
return fields[0], true
|
||||
}
|
||||
|
||||
var fieldCache struct {
|
||||
sync.RWMutex
|
||||
m map[reflect.Type][]field
|
||||
}
|
||||
|
||||
// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
|
||||
func cachedTypeFields(t reflect.Type) []field {
|
||||
fieldCache.RLock()
|
||||
f := fieldCache.m[t]
|
||||
fieldCache.RUnlock()
|
||||
if f != nil {
|
||||
return f
|
||||
}
|
||||
|
||||
// Compute fields without lock.
|
||||
// Might duplicate effort but won't hold other computations back.
|
||||
f = typeFields(t)
|
||||
if f == nil {
|
||||
f = []field{}
|
||||
}
|
||||
|
||||
fieldCache.Lock()
|
||||
if fieldCache.m == nil {
|
||||
fieldCache.m = map[reflect.Type][]field{}
|
||||
}
|
||||
fieldCache.m[t] = f
|
||||
fieldCache.Unlock()
|
||||
return f
|
||||
}
|
||||
|
||||
func isValidTag(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
switch {
|
||||
case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
|
||||
// Backslash and quote chars are reserved, but
|
||||
// otherwise any punctuation chars are allowed
|
||||
// in a tag name.
|
||||
default:
|
||||
if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const (
|
||||
caseMask = ^byte(0x20) // Mask to ignore case in ASCII.
|
||||
kelvin = '\u212a'
|
||||
smallLongEss = '\u017f'
|
||||
)
|
||||
|
||||
// foldFunc returns one of four different case folding equivalence
|
||||
// functions, from most general (and slow) to fastest:
|
||||
//
|
||||
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
|
||||
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
|
||||
// 3) asciiEqualFold, no special, but includes non-letters (including _)
|
||||
// 4) simpleLetterEqualFold, no specials, no non-letters.
|
||||
//
|
||||
// The letters S and K are special because they map to 3 runes, not just 2:
|
||||
// * S maps to s and to U+017F 'ſ' Latin small letter long s
|
||||
// * k maps to K and to U+212A 'K' Kelvin sign
|
||||
// See http://play.golang.org/p/tTxjOc0OGo
|
||||
//
|
||||
// The returned function is specialized for matching against s and
|
||||
// should only be given s. It's not curried for performance reasons.
|
||||
func foldFunc(s []byte) func(s, t []byte) bool {
|
||||
nonLetter := false
|
||||
special := false // special letter
|
||||
for _, b := range s {
|
||||
if b >= utf8.RuneSelf {
|
||||
return bytes.EqualFold
|
||||
}
|
||||
upper := b & caseMask
|
||||
if upper < 'A' || upper > 'Z' {
|
||||
nonLetter = true
|
||||
} else if upper == 'K' || upper == 'S' {
|
||||
// See above for why these letters are special.
|
||||
special = true
|
||||
}
|
||||
}
|
||||
if special {
|
||||
return equalFoldRight
|
||||
}
|
||||
if nonLetter {
|
||||
return asciiEqualFold
|
||||
}
|
||||
return simpleLetterEqualFold
|
||||
}
|
||||
|
||||
// equalFoldRight is a specialization of bytes.EqualFold when s is
|
||||
// known to be all ASCII (including punctuation), but contains an 's',
|
||||
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
|
||||
// See comments on foldFunc.
|
||||
func equalFoldRight(s, t []byte) bool {
|
||||
for _, sb := range s {
|
||||
if len(t) == 0 {
|
||||
return false
|
||||
}
|
||||
tb := t[0]
|
||||
if tb < utf8.RuneSelf {
|
||||
if sb != tb {
|
||||
sbUpper := sb & caseMask
|
||||
if 'A' <= sbUpper && sbUpper <= 'Z' {
|
||||
if sbUpper != tb&caseMask {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
t = t[1:]
|
||||
continue
|
||||
}
|
||||
// sb is ASCII and t is not. t must be either kelvin
|
||||
// sign or long s; sb must be s, S, k, or K.
|
||||
tr, size := utf8.DecodeRune(t)
|
||||
switch sb {
|
||||
case 's', 'S':
|
||||
if tr != smallLongEss {
|
||||
return false
|
||||
}
|
||||
case 'k', 'K':
|
||||
if tr != kelvin {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
t = t[size:]
|
||||
|
||||
}
|
||||
if len(t) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// asciiEqualFold is a specialization of bytes.EqualFold for use when
|
||||
// s is all ASCII (but may contain non-letters) and contains no
|
||||
// special-folding letters.
|
||||
// See comments on foldFunc.
|
||||
func asciiEqualFold(s, t []byte) bool {
|
||||
if len(s) != len(t) {
|
||||
return false
|
||||
}
|
||||
for i, sb := range s {
|
||||
tb := t[i]
|
||||
if sb == tb {
|
||||
continue
|
||||
}
|
||||
if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
|
||||
if sb&caseMask != tb&caseMask {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// simpleLetterEqualFold is a specialization of bytes.EqualFold for
|
||||
// use when s is all ASCII letters (no underscores, etc) and also
|
||||
// doesn't contain 'k', 'K', 's', or 'S'.
|
||||
// See comments on foldFunc.
|
||||
func simpleLetterEqualFold(s, t []byte) bool {
|
||||
if len(s) != len(t) {
|
||||
return false
|
||||
}
|
||||
for i, b := range s {
|
||||
if b&caseMask != t[i]&caseMask {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// tagOptions is the string following a comma in a struct field's "json"
|
||||
// tag, or the empty string. It does not include the leading comma.
|
||||
type tagOptions string
|
||||
|
||||
// parseTag splits a struct field's json tag into its name and
|
||||
// comma-separated options.
|
||||
func parseTag(tag string) (string, tagOptions) {
|
||||
if idx := strings.Index(tag, ","); idx != -1 {
|
||||
return tag[:idx], tagOptions(tag[idx+1:])
|
||||
}
|
||||
return tag, tagOptions("")
|
||||
}
|
||||
|
||||
// Contains reports whether a comma-separated list of options
|
||||
// contains a particular substr flag. substr must be surrounded by a
|
||||
// string boundary or commas.
|
||||
func (o tagOptions) Contains(optionName string) bool {
|
||||
if len(o) == 0 {
|
||||
return false
|
||||
}
|
||||
s := string(o)
|
||||
for s != "" {
|
||||
var next string
|
||||
i := strings.Index(s, ",")
|
||||
if i >= 0 {
|
||||
s, next = s[:i], s[i+1:]
|
||||
}
|
||||
if s == optionName {
|
||||
return true
|
||||
}
|
||||
s = next
|
||||
}
|
||||
return false
|
||||
}
|
469
pkg/util/strategicpatch/patch.go
Normal file
469
pkg/util/strategicpatch/patch.go
Normal file
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 strategicpatch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// An alternate implementation of JSON Merge Patch
|
||||
// (https://tools.ietf.org/html/rfc7386) which supports the ability to annotate
|
||||
// certain fields with metadata that indicates whether the elements of JSON
|
||||
// lists should be merged or replaced.
|
||||
//
|
||||
// For more information, see the PATCH section of docs/api-conventions.md.
|
||||
func StrategicMergePatchData(original, patch []byte, dataStruct interface{}) ([]byte, error) {
|
||||
var o map[string]interface{}
|
||||
err := json.Unmarshal(original, &o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var p map[string]interface{}
|
||||
err = json.Unmarshal(patch, &p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(dataStruct)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("strategic merge patch needs a struct, %s received instead", t.Kind().String())
|
||||
}
|
||||
|
||||
result, err := mergeMap(o, p, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(result)
|
||||
}
|
||||
|
||||
const specialKey = "$patch"
|
||||
|
||||
// Merge fields from a patch map into the original map. Note: This may modify
|
||||
// both the original map and the patch because getting a deep copy of a map in
|
||||
// golang is highly non-trivial.
|
||||
func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[string]interface{}, error) {
|
||||
// If the map contains "$patch: replace", don't merge it, just use the
|
||||
// patch map directly. Later on, can add a non-recursive replace that only
|
||||
// affects the map that the $patch is in.
|
||||
if v, ok := patch[specialKey]; ok {
|
||||
if v == "replace" {
|
||||
delete(patch, specialKey)
|
||||
return patch, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown patch type found: %s", v)
|
||||
}
|
||||
|
||||
// nil is an accepted value for original to simplify logic in other places.
|
||||
// If original is nil, create a map so if patch requires us to modify the
|
||||
// map, it'll work.
|
||||
if original == nil {
|
||||
original = map[string]interface{}{}
|
||||
}
|
||||
|
||||
// Start merging the patch into the original.
|
||||
for k, patchV := range patch {
|
||||
// If the value of this key is null, delete the key if it exists in the
|
||||
// original. Otherwise, skip it.
|
||||
if patchV == nil {
|
||||
if _, ok := original[k]; ok {
|
||||
delete(original, k)
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, ok := original[k]
|
||||
if !ok {
|
||||
// If it's not in the original document, just take the patch value.
|
||||
original[k] = patchV
|
||||
continue
|
||||
}
|
||||
// If they're both maps or lists, recurse into the value.
|
||||
// First find the fieldPatchStrategy and fieldPatchMergeKey.
|
||||
fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := lookupPatchMetadata(t, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
originalType := reflect.TypeOf(original[k])
|
||||
patchType := reflect.TypeOf(patchV)
|
||||
if originalType == patchType {
|
||||
if originalType.Kind() == reflect.Map && fieldPatchStrategy != "replace" {
|
||||
typedOriginal := original[k].(map[string]interface{})
|
||||
typedPatch := patchV.(map[string]interface{})
|
||||
var err error
|
||||
original[k], err = mergeMap(typedOriginal, typedPatch, fieldType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if originalType.Kind() == reflect.Slice && fieldPatchStrategy == "merge" {
|
||||
elemType := fieldType.Elem()
|
||||
typedOriginal := original[k].([]interface{})
|
||||
typedPatch := patchV.([]interface{})
|
||||
var err error
|
||||
original[k], err = mergeSlice(typedOriginal, typedPatch, elemType, fieldPatchMergeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If originalType and patchType are different OR the types are both
|
||||
// maps or slices but we're just supposed to replace them, just take
|
||||
// the value from patch.
|
||||
original[k] = patchV
|
||||
}
|
||||
|
||||
return original, nil
|
||||
}
|
||||
|
||||
// Merge two slices together. Note: This may modify both the original slice and
|
||||
// the patch because getting a deep copy of a slice in golang is highly
|
||||
// non-trivial.
|
||||
func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey string) ([]interface{}, error) {
|
||||
if len(original) == 0 && len(patch) == 0 {
|
||||
return original, nil
|
||||
}
|
||||
// All the values must be of the same type, but not a list.
|
||||
t, err := sliceElementType(original, patch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("types of list elements need to be the same, type: %s: %v",
|
||||
elemType.Kind().String(), err)
|
||||
}
|
||||
if t.Kind() == reflect.Slice {
|
||||
return nil, fmt.Errorf("not supporting merging lists of lists yet")
|
||||
}
|
||||
// If the elements are not maps, merge the slices of scalars.
|
||||
if t.Kind() != reflect.Map {
|
||||
// Maybe in the future add a "concat" mode that doesn't
|
||||
// uniqify.
|
||||
both := append(original, patch...)
|
||||
return uniqifyScalars(both), nil
|
||||
}
|
||||
|
||||
if mergeKey == "" {
|
||||
return nil, fmt.Errorf("cannot merge lists without merge key for type %s", elemType.Kind().String())
|
||||
}
|
||||
|
||||
// First look for any special $patch elements.
|
||||
patchWithoutSpecialElements := []interface{}{}
|
||||
replace := false
|
||||
for _, v := range patch {
|
||||
typedV := v.(map[string]interface{})
|
||||
patchType, ok := typedV[specialKey]
|
||||
if ok {
|
||||
if patchType == "delete" {
|
||||
mergeValue, ok := typedV[mergeKey]
|
||||
if ok {
|
||||
_, originalKey, found := findMapInSliceBasedOnKeyValue(original, mergeKey, mergeValue)
|
||||
if found {
|
||||
// Delete the element at originalKey.
|
||||
original = append(original[:originalKey], original[originalKey+1:]...)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("delete patch type with no merge key defined")
|
||||
}
|
||||
} else if patchType == "replace" {
|
||||
replace = true
|
||||
// Continue iterating through the array to prune any other $patch elements.
|
||||
} else if patchType == "merge" {
|
||||
return nil, fmt.Errorf("merging lists cannot yet be specified in the patch")
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown patch type found: %s", patchType)
|
||||
}
|
||||
} else {
|
||||
patchWithoutSpecialElements = append(patchWithoutSpecialElements, v)
|
||||
}
|
||||
}
|
||||
|
||||
if replace {
|
||||
return patchWithoutSpecialElements, nil
|
||||
}
|
||||
|
||||
patch = patchWithoutSpecialElements
|
||||
|
||||
// Merge patch into original.
|
||||
for _, v := range patch {
|
||||
// Because earlier we confirmed that all the elements are maps.
|
||||
typedV := v.(map[string]interface{})
|
||||
mergeValue, ok := typedV[mergeKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("all list elements need the merge key %s", mergeKey)
|
||||
}
|
||||
// If we find a value with this merge key value in original, merge the
|
||||
// maps. Otherwise append onto original.
|
||||
originalMap, originalKey, found := findMapInSliceBasedOnKeyValue(original, mergeKey, mergeValue)
|
||||
if found {
|
||||
var mergedMaps interface{}
|
||||
var err error
|
||||
// Merge into original.
|
||||
mergedMaps, err = mergeMap(originalMap, typedV, elemType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
original[originalKey] = mergedMaps
|
||||
} else {
|
||||
original = append(original, v)
|
||||
}
|
||||
}
|
||||
|
||||
return original, nil
|
||||
}
|
||||
|
||||
// This panics if any element of the slice is not a map.
|
||||
func findMapInSliceBasedOnKeyValue(m []interface{}, key string, value interface{}) (map[string]interface{}, int, bool) {
|
||||
for k, v := range m {
|
||||
typedV := v.(map[string]interface{})
|
||||
valueToMatch, ok := typedV[key]
|
||||
if ok && valueToMatch == value {
|
||||
return typedV, k, true
|
||||
}
|
||||
}
|
||||
return nil, 0, false
|
||||
}
|
||||
|
||||
// This function takes a JSON map and sorts all the lists that should be merged
|
||||
// by key. This is needed by tests because in JSON, list order is significant,
|
||||
// but in Strategic Merge Patch, merge lists do not have significant order.
|
||||
// Sorting the lists allows for order-insensitive comparison of patched maps.
|
||||
func sortMergeListsByName(mapJSON []byte, dataStruct interface{}) ([]byte, error) {
|
||||
var m map[string]interface{}
|
||||
err := json.Unmarshal(mapJSON, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newM, err := sortMergeListsByNameMap(m, reflect.TypeOf(dataStruct))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(newM)
|
||||
}
|
||||
|
||||
func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[string]interface{}, error) {
|
||||
newS := map[string]interface{}{}
|
||||
for k, v := range s {
|
||||
fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := lookupPatchMetadata(t, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If v is a map or a merge slice, recurse.
|
||||
if typedV, ok := v.(map[string]interface{}); ok {
|
||||
var err error
|
||||
v, err = sortMergeListsByNameMap(typedV, fieldType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if typedV, ok := v.([]interface{}); ok {
|
||||
if fieldPatchStrategy == "merge" {
|
||||
var err error
|
||||
v, err = sortMergeListsByNameArray(typedV, fieldType.Elem(), fieldPatchMergeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
newS[k] = v
|
||||
}
|
||||
return newS, nil
|
||||
}
|
||||
|
||||
func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey string) ([]interface{}, error) {
|
||||
if len(s) == 0 {
|
||||
return s, nil
|
||||
}
|
||||
// We don't support lists of lists yet.
|
||||
t, err := sliceElementType(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.Kind() == reflect.Slice {
|
||||
return nil, fmt.Errorf("not supporting lists of lists yet")
|
||||
}
|
||||
|
||||
// If the elements are not maps...
|
||||
if t.Kind() != reflect.Map {
|
||||
// Sort the elements, because they may have been merged out of order.
|
||||
return uniqifyAndSortScalars(s), nil
|
||||
}
|
||||
|
||||
// Elements are maps - if one of the keys of the map is a map or a
|
||||
// list, we need to recurse into it.
|
||||
newS := []interface{}{}
|
||||
for _, elem := range s {
|
||||
typedElem := elem.(map[string]interface{})
|
||||
newElem, err := sortMergeListsByNameMap(typedElem, elemType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newS = append(newS, newElem)
|
||||
}
|
||||
|
||||
// Sort the maps.
|
||||
newS = sortMapsBasedOnField(newS, mergeKey)
|
||||
return newS, nil
|
||||
}
|
||||
|
||||
func sortMapsBasedOnField(m []interface{}, fieldName string) []interface{} {
|
||||
mapM := []map[string]interface{}{}
|
||||
for _, v := range m {
|
||||
mapM = append(mapM, v.(map[string]interface{}))
|
||||
}
|
||||
ss := SortableSliceOfMaps{mapM, fieldName}
|
||||
sort.Sort(ss)
|
||||
newM := []interface{}{}
|
||||
for _, v := range ss.s {
|
||||
newM = append(newM, v)
|
||||
}
|
||||
return newM
|
||||
}
|
||||
|
||||
type SortableSliceOfMaps struct {
|
||||
s []map[string]interface{}
|
||||
k string // key to sort on
|
||||
}
|
||||
|
||||
func (ss SortableSliceOfMaps) Len() int {
|
||||
return len(ss.s)
|
||||
}
|
||||
|
||||
func (ss SortableSliceOfMaps) Less(i, j int) bool {
|
||||
iStr := fmt.Sprintf("%v", ss.s[i][ss.k])
|
||||
jStr := fmt.Sprintf("%v", ss.s[j][ss.k])
|
||||
return sort.StringsAreSorted([]string{iStr, jStr})
|
||||
}
|
||||
|
||||
func (ss SortableSliceOfMaps) Swap(i, j int) {
|
||||
tmp := ss.s[i]
|
||||
ss.s[i] = ss.s[j]
|
||||
ss.s[j] = tmp
|
||||
}
|
||||
|
||||
func uniqifyAndSortScalars(s []interface{}) []interface{} {
|
||||
s = uniqifyScalars(s)
|
||||
|
||||
ss := SortableSliceOfScalars{s}
|
||||
sort.Sort(ss)
|
||||
return ss.s
|
||||
}
|
||||
|
||||
func uniqifyScalars(s []interface{}) []interface{} {
|
||||
// Clever algorithm to uniqify.
|
||||
length := len(s) - 1
|
||||
for i := 0; i < length; i++ {
|
||||
for j := i + 1; j <= length; j++ {
|
||||
if s[i] == s[j] {
|
||||
s[j] = s[length]
|
||||
s = s[0:length]
|
||||
length--
|
||||
j--
|
||||
}
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type SortableSliceOfScalars struct {
|
||||
s []interface{}
|
||||
}
|
||||
|
||||
func (ss SortableSliceOfScalars) Len() int {
|
||||
return len(ss.s)
|
||||
}
|
||||
|
||||
func (ss SortableSliceOfScalars) Less(i, j int) bool {
|
||||
iStr := fmt.Sprintf("%v", ss.s[i])
|
||||
jStr := fmt.Sprintf("%v", ss.s[j])
|
||||
return sort.StringsAreSorted([]string{iStr, jStr})
|
||||
}
|
||||
|
||||
func (ss SortableSliceOfScalars) Swap(i, j int) {
|
||||
tmp := ss.s[i]
|
||||
ss.s[i] = ss.s[j]
|
||||
ss.s[j] = tmp
|
||||
}
|
||||
|
||||
// Returns the type of the elements of N slice(s). If the type is different,
|
||||
// returns an error.
|
||||
func sliceElementType(slices ...[]interface{}) (reflect.Type, error) {
|
||||
var prevType reflect.Type
|
||||
for _, s := range slices {
|
||||
// Go through elements of all given slices and make sure they are all the same type.
|
||||
for _, v := range s {
|
||||
currentType := reflect.TypeOf(v)
|
||||
if prevType == nil {
|
||||
prevType = currentType
|
||||
} else {
|
||||
if prevType != currentType {
|
||||
return nil, fmt.Errorf("at least two types found: %s and %s", prevType, currentType)
|
||||
}
|
||||
prevType = currentType
|
||||
}
|
||||
}
|
||||
}
|
||||
if prevType == nil {
|
||||
return nil, fmt.Errorf("no elements in any given slices")
|
||||
}
|
||||
return prevType, nil
|
||||
}
|
||||
|
||||
// Finds the patchStrategy and patchMergeKey struct tag fields on a given
|
||||
// struct field given the struct type and the JSON name of the field.
|
||||
func lookupPatchMetadata(t reflect.Type, jsonField string) (reflect.Type, string, string, error) {
|
||||
if t.Kind() == reflect.Map {
|
||||
return t.Elem(), "", "", nil
|
||||
}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, "", "", fmt.Errorf("merging an object in json but data type is not map or struct, instead is: %s",
|
||||
t.Kind().String())
|
||||
}
|
||||
jf := []byte(jsonField)
|
||||
// Find the field that the JSON library would use.
|
||||
var f *field
|
||||
fields := cachedTypeFields(t)
|
||||
for i := range fields {
|
||||
ff := &fields[i]
|
||||
if bytes.Equal(ff.nameBytes, jf) {
|
||||
f = ff
|
||||
break
|
||||
}
|
||||
// Do case-insensitive comparison.
|
||||
if f == nil && ff.equalFold(ff.nameBytes, jf) {
|
||||
f = ff
|
||||
}
|
||||
}
|
||||
if f != nil {
|
||||
// Find the reflect.Value of the most preferential
|
||||
// struct field.
|
||||
tjf := t.Field(f.index[0])
|
||||
patchStrategy := tjf.Tag.Get("patchStrategy")
|
||||
patchMergeKey := tjf.Tag.Get("patchMergeKey")
|
||||
return tjf.Type, patchStrategy, patchMergeKey, nil
|
||||
}
|
||||
return nil, "", "", fmt.Errorf("unable to find api field in struct %s for the json field %q", t.Name(), jsonField)
|
||||
}
|
433
pkg/util/strategicpatch/patch_test.go
Normal file
433
pkg/util/strategicpatch/patch_test.go
Normal file
@@ -0,0 +1,433 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. All rights reserved.
|
||||
|
||||
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 strategicpatch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
type TestCases struct {
|
||||
StrategicMergePatchCases []StrategicMergePatchCase
|
||||
SortMergeListTestCases []SortMergeListCase
|
||||
}
|
||||
|
||||
type StrategicMergePatchCase struct {
|
||||
Description string
|
||||
Patch map[string]interface{}
|
||||
Original map[string]interface{}
|
||||
Result map[string]interface{}
|
||||
}
|
||||
|
||||
type SortMergeListCase struct {
|
||||
Description string
|
||||
Original map[string]interface{}
|
||||
Sorted map[string]interface{}
|
||||
}
|
||||
|
||||
type MergeItem struct {
|
||||
Name string
|
||||
Value string
|
||||
MergingList []MergeItem `patchStrategy:"merge" patchMergeKey:"name"`
|
||||
NonMergingList []MergeItem
|
||||
MergingIntList []int `patchStrategy:"merge"`
|
||||
NonMergingIntList []int
|
||||
SimpleMap map[string]string
|
||||
}
|
||||
|
||||
var testCaseData = []byte(`
|
||||
strategicMergePatchCases:
|
||||
- description: add new field
|
||||
original:
|
||||
name: 1
|
||||
patch:
|
||||
value: 1
|
||||
result:
|
||||
name: 1
|
||||
value: 1
|
||||
- description: remove field and add new field
|
||||
original:
|
||||
name: 1
|
||||
patch:
|
||||
name: null
|
||||
value: 1
|
||||
result:
|
||||
value: 1
|
||||
- description: merge arrays of scalars
|
||||
original:
|
||||
mergingIntList:
|
||||
- 1
|
||||
- 2
|
||||
patch:
|
||||
mergingIntList:
|
||||
- 2
|
||||
- 3
|
||||
result:
|
||||
mergingIntList:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- description: replace arrays of scalars
|
||||
original:
|
||||
nonMergingIntList:
|
||||
- 1
|
||||
- 2
|
||||
patch:
|
||||
nonMergingIntList:
|
||||
- 2
|
||||
- 3
|
||||
result:
|
||||
nonMergingIntList:
|
||||
- 2
|
||||
- 3
|
||||
- description: update param of list that should be merged but had element added serverside
|
||||
original:
|
||||
mergingList:
|
||||
- name: 1
|
||||
value: 1
|
||||
- name: 2
|
||||
value: 2
|
||||
patch:
|
||||
mergingList:
|
||||
- name: 1
|
||||
value: a
|
||||
result:
|
||||
mergingList:
|
||||
- name: 1
|
||||
value: a
|
||||
- name: 2
|
||||
value: 2
|
||||
- description: delete field when field is nested in a map
|
||||
original:
|
||||
simpleMap:
|
||||
key1: 1
|
||||
key2: 1
|
||||
patch:
|
||||
simpleMap:
|
||||
key2: null
|
||||
result:
|
||||
simpleMap:
|
||||
key1: 1
|
||||
- description: update nested list when nested list should not be merged
|
||||
original:
|
||||
mergingList:
|
||||
- name: 1
|
||||
nonMergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
value: 2
|
||||
- name: 2
|
||||
patch:
|
||||
mergingList:
|
||||
- name: 1
|
||||
nonMergingList:
|
||||
- name: 1
|
||||
value: 1
|
||||
result:
|
||||
mergingList:
|
||||
- name: 1
|
||||
nonMergingList:
|
||||
- name: 1
|
||||
value: 1
|
||||
- name: 2
|
||||
- description: update nested list when nested list should be merged
|
||||
original:
|
||||
mergingList:
|
||||
- name: 1
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
value: 2
|
||||
- name: 2
|
||||
patch:
|
||||
mergingList:
|
||||
- name: 1
|
||||
mergingList:
|
||||
- name: 1
|
||||
value: 1
|
||||
result:
|
||||
mergingList:
|
||||
- name: 1
|
||||
mergingList:
|
||||
- name: 1
|
||||
value: 1
|
||||
- name: 2
|
||||
value: 2
|
||||
- name: 2
|
||||
- description: update map when map should be replaced
|
||||
original:
|
||||
name: 1
|
||||
value: 1
|
||||
patch:
|
||||
value: 1
|
||||
$patch: replace
|
||||
result:
|
||||
value: 1
|
||||
- description: merge empty merge lists
|
||||
original:
|
||||
mergingList: []
|
||||
patch:
|
||||
mergingList: []
|
||||
result:
|
||||
mergingList: []
|
||||
- description: delete others in a map
|
||||
original:
|
||||
name: 1
|
||||
value: 1
|
||||
patch:
|
||||
$patch: replace
|
||||
result: {}
|
||||
- description: delete item from a merge list
|
||||
original:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
patch:
|
||||
mergingList:
|
||||
- $patch: delete
|
||||
name: 1
|
||||
result:
|
||||
mergingList:
|
||||
- name: 2
|
||||
- description: add and delete item from a merge list
|
||||
original:
|
||||
merginglist:
|
||||
- name: 1
|
||||
- name: 2
|
||||
patch:
|
||||
merginglist:
|
||||
- name: 3
|
||||
- $patch: delete
|
||||
name: 1
|
||||
result:
|
||||
merginglist:
|
||||
- name: 2
|
||||
- name: 3
|
||||
- description: delete all items from a merge list
|
||||
original:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
patch:
|
||||
mergingList:
|
||||
- $patch: replace
|
||||
result:
|
||||
mergingList: []
|
||||
sortMergeListTestCases:
|
||||
- description: sort one list of maps
|
||||
original:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 3
|
||||
- name: 2
|
||||
sorted:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
- name: 3
|
||||
- description: sort lists of maps but not nested lists of maps
|
||||
original:
|
||||
mergingList:
|
||||
- name: 2
|
||||
nonMergingList:
|
||||
- name: 1
|
||||
- name: 3
|
||||
- name: 2
|
||||
- name: 1
|
||||
nonMergingList:
|
||||
- name: 2
|
||||
- name: 1
|
||||
sorted:
|
||||
mergingList:
|
||||
- name: 1
|
||||
nonMergingList:
|
||||
- name: 2
|
||||
- name: 1
|
||||
- name: 2
|
||||
nonMergingList:
|
||||
- name: 1
|
||||
- name: 3
|
||||
- name: 2
|
||||
- description: sort lists of maps and nested lists of maps
|
||||
fieldTypes:
|
||||
original:
|
||||
mergingList:
|
||||
- name: 2
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 3
|
||||
- name: 2
|
||||
- name: 1
|
||||
mergingList:
|
||||
- name: 2
|
||||
- name: 1
|
||||
sorted:
|
||||
mergingList:
|
||||
- name: 1
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
- name: 2
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
- name: 3
|
||||
- description: merging list should NOT sort when nested in a non merging list
|
||||
original:
|
||||
nonMergingList:
|
||||
- name: 2
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 3
|
||||
- name: 2
|
||||
- name: 1
|
||||
mergingList:
|
||||
- name: 2
|
||||
- name: 1
|
||||
sorted:
|
||||
nonMergingList:
|
||||
- name: 2
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 3
|
||||
- name: 2
|
||||
- name: 1
|
||||
mergingList:
|
||||
- name: 2
|
||||
- name: 1
|
||||
- description: sort a very nested list of maps
|
||||
fieldTypes:
|
||||
original:
|
||||
mergingList:
|
||||
- mergingList:
|
||||
- mergingList:
|
||||
- name: 2
|
||||
- name: 1
|
||||
sorted:
|
||||
mergingList:
|
||||
- mergingList:
|
||||
- mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
- description: sort nested lists of ints
|
||||
original:
|
||||
mergingList:
|
||||
- name: 2
|
||||
mergingIntList:
|
||||
- 1
|
||||
- 3
|
||||
- 2
|
||||
- name: 1
|
||||
mergingIntList:
|
||||
- 2
|
||||
- 1
|
||||
sorted:
|
||||
mergingList:
|
||||
- name: 1
|
||||
mergingIntList:
|
||||
- 1
|
||||
- 2
|
||||
- name: 2
|
||||
mergingIntList:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`)
|
||||
|
||||
func TestStrategicMergePatch(t *testing.T) {
|
||||
tc := TestCases{}
|
||||
err := yaml.Unmarshal(testCaseData, &tc)
|
||||
if err != nil {
|
||||
t.Errorf("can't unmarshal test cases: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var e MergeItem
|
||||
for _, c := range tc.StrategicMergePatchCases {
|
||||
result, err := StrategicMergePatchData(toJSON(c.Original), toJSON(c.Patch), e)
|
||||
if err != nil {
|
||||
t.Errorf("error patching: %v:\noriginal:\n%s\npatch:\n%s",
|
||||
err, toYAML(c.Original), toYAML(c.Patch))
|
||||
}
|
||||
|
||||
// Sort the lists that have merged maps, since order is not significant.
|
||||
result, err = sortMergeListsByName(result, e)
|
||||
if err != nil {
|
||||
t.Errorf("error sorting result object: %v", err)
|
||||
}
|
||||
cResult, err := sortMergeListsByName(toJSON(c.Result), e)
|
||||
if err != nil {
|
||||
t.Errorf("error sorting result object: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, cResult) {
|
||||
t.Errorf("patching failed: %s\noriginal:\n%s\npatch:\n%s\nexpected result:\n%s\ngot result:\n%s",
|
||||
c.Description, toYAML(c.Original), toYAML(c.Patch), jsonToYAML(cResult), jsonToYAML(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortMergeLists(t *testing.T) {
|
||||
tc := TestCases{}
|
||||
err := yaml.Unmarshal(testCaseData, &tc)
|
||||
if err != nil {
|
||||
t.Errorf("can't unmarshal test cases: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var e MergeItem
|
||||
for _, c := range tc.SortMergeListTestCases {
|
||||
sorted, err := sortMergeListsByName(toJSON(c.Original), e)
|
||||
if err != nil {
|
||||
t.Errorf("sort arrays returned error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(sorted, toJSON(c.Sorted)) {
|
||||
t.Errorf("sorting failed: %s\ntried to sort:\n%s\nexpected:\n%s\ngot:\n%s",
|
||||
c.Description, toYAML(c.Original), toYAML(c.Sorted), jsonToYAML(sorted))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toYAML(v interface{}) string {
|
||||
y, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("yaml marshal failed: %v", err))
|
||||
}
|
||||
return string(y)
|
||||
}
|
||||
|
||||
func toJSON(v interface{}) []byte {
|
||||
j, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("json marshal failed: %s", spew.Sdump(v)))
|
||||
}
|
||||
return j
|
||||
}
|
||||
|
||||
func jsonToYAML(j []byte) []byte {
|
||||
y, err := yaml.JSONToYAML(j)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("json to yaml failed: %v", err))
|
||||
}
|
||||
return y
|
||||
}
|
Reference in New Issue
Block a user