Commit 5869ce6d authored by Antoine Pelisse's avatar Antoine Pelisse

Add structured-merge-diff vendor

This is where most of the logic actually lives.
parent 732cb100
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"element.go",
"fromvalue.go",
"managers.go",
"path.go",
"set.go",
],
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/fieldpath",
importpath = "sigs.k8s.io/structured-merge-diff/fieldpath",
visibility = ["//visibility:public"],
deps = ["//vendor/sigs.k8s.io/structured-merge-diff/value:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
Copyright 2018 The Kubernetes 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 fieldpath defines a way for referencing path elements (e.g., an
// index in an array, or a key in a map). It provides types for arranging these
// into paths for referencing nested fields, and for grouping those into sets,
// for referencing multiple nested fields.
package fieldpath
/*
Copyright 2018 The Kubernetes 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 fieldpath
import (
"fmt"
"sort"
"strings"
"sigs.k8s.io/structured-merge-diff/value"
)
// PathElement describes how to select a child field given a containing object.
type PathElement struct {
// Exactly one of the following fields should be non-nil.
// FieldName selects a single field from a map (reminder: this is also
// how structs are represented). The containing object must be a map.
FieldName *string
// Key selects the list element which has fields matching those given.
// The containing object must be an associative list with map typed
// elements.
Key []value.Field
// Value selects the list element with the given value. The containing
// object must be an associative list with a primitive typed element
// (i.e., a set).
Value *value.Value
// Index selects a list element by its index number. The containing
// object must be an atomic list.
Index *int
}
// String presents the path element as a human-readable string.
func (e PathElement) String() string {
switch {
case e.FieldName != nil:
return "." + *e.FieldName
case len(e.Key) > 0:
strs := make([]string, len(e.Key))
for i, k := range e.Key {
strs[i] = fmt.Sprintf("%v=%v", k.Name, k.Value)
}
// The order must be canonical, since we use the string value
// in a set structure.
sort.Strings(strs)
return "[" + strings.Join(strs, ",") + "]"
case e.Value != nil:
return fmt.Sprintf("[=%v]", e.Value)
case e.Index != nil:
return fmt.Sprintf("[%v]", *e.Index)
default:
return "{{invalid path element}}"
}
}
// KeyByFields is a helper function which constructs a key for an associative
// list type. `nameValues` must have an even number of entries, alternating
// names (type must be string) with values (type must be value.Value). If these
// conditions are not met, KeyByFields will panic--it's intended for static
// construction and shouldn't have user-produced values passed to it.
func KeyByFields(nameValues ...interface{}) []value.Field {
if len(nameValues)%2 != 0 {
panic("must have a value for every name")
}
out := []value.Field{}
for i := 0; i < len(nameValues)-1; i += 2 {
out = append(out, value.Field{
Name: nameValues[i].(string),
Value: nameValues[i+1].(value.Value),
})
}
return out
}
// PathElementSet is a set of path elements.
// TODO: serialize as a list.
type PathElementSet struct {
// The strange construction is because there's no way to test
// PathElements for equality (it can't be used as a key for a map).
members map[string]PathElement
}
// Insert adds pe to the set.
func (s *PathElementSet) Insert(pe PathElement) {
serialized := pe.String()
if s.members == nil {
s.members = map[string]PathElement{
serialized: pe,
}
return
}
if _, ok := s.members[serialized]; !ok {
s.members[serialized] = pe
}
}
// Union returns a set containing elements that appear in either s or s2.
func (s *PathElementSet) Union(s2 *PathElementSet) *PathElementSet {
out := &PathElementSet{
members: map[string]PathElement{},
}
for k, v := range s.members {
out.members[k] = v
}
for k, v := range s2.members {
out.members[k] = v
}
return out
}
// Intersection returns a set containing elements which appear in both s and s2.
func (s *PathElementSet) Intersection(s2 *PathElementSet) *PathElementSet {
out := &PathElementSet{
members: map[string]PathElement{},
}
for k, v := range s.members {
if _, ok := s2.members[k]; ok {
out.members[k] = v
}
}
return out
}
// Difference returns a set containing elements which appear in s but not in s2.
func (s *PathElementSet) Difference(s2 *PathElementSet) *PathElementSet {
out := &PathElementSet{
members: map[string]PathElement{},
}
for k, v := range s.members {
if _, ok := s2.members[k]; !ok {
out.members[k] = v
}
}
return out
}
// Size retuns the number of elements in the set.
func (s *PathElementSet) Size() int { return len(s.members) }
// Has returns true if pe is a member of the set.
func (s *PathElementSet) Has(pe PathElement) bool {
if s.members == nil {
return false
}
_, ok := s.members[pe.String()]
return ok
}
// Equals returns true if s and s2 have exactly the same members.
func (s *PathElementSet) Equals(s2 *PathElementSet) bool {
if len(s.members) != len(s2.members) {
return false
}
for k := range s.members {
if _, ok := s2.members[k]; !ok {
return false
}
}
return true
}
// Iterate calls f for each PathElement in the set.
func (s *PathElementSet) Iterate(f func(PathElement)) {
for _, pe := range s.members {
f(pe)
}
}
/*
Copyright 2018 The Kubernetes 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 fieldpath
import (
"sigs.k8s.io/structured-merge-diff/value"
)
// SetFromValue creates a set containing every leaf field mentioned in v.
func SetFromValue(v value.Value) *Set {
s := NewSet()
w := objectWalker{
path: Path{},
value: v,
do: func(p Path) { s.Insert(p) },
}
w.walk()
return s
}
type objectWalker struct {
path Path
value value.Value
do func(Path)
}
func (w *objectWalker) walk() {
switch {
case w.value.Null:
case w.value.FloatValue != nil:
case w.value.IntValue != nil:
case w.value.StringValue != nil:
case w.value.BooleanValue != nil:
// All leaf fields handled the same way (after the switch
// statement).
// Descend
case w.value.ListValue != nil:
// If the list were atomic, we'd break here, but we don't have
// a schema, so we can't tell.
for i, child := range w.value.ListValue.Items {
w2 := *w
w2.path = append(w.path, GuessBestListPathElement(i, child))
w2.value = child
w2.walk()
}
return
case w.value.MapValue != nil:
// If the map/struct were atomic, we'd break here, but we don't
// have a schema, so we can't tell.
for i := range w.value.MapValue.Items {
child := w.value.MapValue.Items[i]
w2 := *w
w2.path = append(w.path, PathElement{FieldName: &child.Name})
w2.value = child.Value
w2.walk()
}
return
}
// Leaf fields get added to the set.
if len(w.path) > 0 {
w.do(w.path)
}
}
// AssociativeListCandidateFieldNames lists the field names which are
// considered keys if found in a list element.
var AssociativeListCandidateFieldNames = []string{
"key",
"id",
"name",
}
// GuessBestListPathElement guesses whether item is an associative list
// element, which should be referenced by key(s), or if it is not and therefore
// referencing by index is acceptable. Currently this is done by checking
// whether item has any of the fields listed in
// AssociativeListCandidateFieldNames which have scalar values.
func GuessBestListPathElement(index int, item value.Value) PathElement {
if item.MapValue == nil {
// Non map items could be parts of sets or regular "atomic"
// lists. We won't try to guess whether something should be a
// set or not.
return PathElement{Index: &index}
}
var keys []value.Field
for _, name := range AssociativeListCandidateFieldNames {
f, ok := item.MapValue.Get(name)
if !ok {
continue
}
// only accept primitive/scalar types as keys.
if f.Value.Null || f.Value.MapValue != nil || f.Value.ListValue != nil {
continue
}
keys = append(keys, *f)
}
if len(keys) > 0 {
return PathElement{Key: keys}
}
return PathElement{Index: &index}
}
/*
Copyright 2018 The Kubernetes 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 fieldpath
// APIVersion describes the version of an object or of a fieldset.
type APIVersion string
// VersionedSet associates a version to a set.
type VersionedSet struct {
*Set
APIVersion APIVersion
}
// ManagedFields is a map from manager to VersionedSet (what they own in
// what version).
type ManagedFields map[string]*VersionedSet
// Difference returns a symmetric difference between two Managers. If a
// given user's entry has version X in lhs and version Y in rhs, then
// the return value for that user will be from rhs. If the difference for
// a user is an empty set, that user will not be inserted in the map.
func (lhs ManagedFields) Difference(rhs ManagedFields) ManagedFields {
diff := ManagedFields{}
for manager, left := range lhs {
right, ok := rhs[manager]
if !ok {
if !left.Empty() {
diff[manager] = left
}
continue
}
// If we have sets in both but their version
// differs, we don't even diff and keep the
// entire thing.
if left.APIVersion != right.APIVersion {
diff[manager] = right
continue
}
newSet := left.Difference(right.Set).Union(right.Difference(left.Set))
if !newSet.Empty() {
diff[manager] = &VersionedSet{
Set: newSet,
APIVersion: right.APIVersion,
}
}
}
for manager, set := range rhs {
if _, ok := lhs[manager]; ok {
// Already done
continue
}
if !set.Empty() {
diff[manager] = set
}
}
return diff
}
/*
Copyright 2018 The Kubernetes 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 fieldpath
import (
"fmt"
"strings"
"sigs.k8s.io/structured-merge-diff/value"
)
// Path describes how to select a potentially deeply-nested child field given a
// containing object.
type Path []PathElement
func (fp Path) String() string {
strs := make([]string, len(fp))
for i := range fp {
strs[i] = fp[i].String()
}
return strings.Join(strs, "")
}
// MakePath constructs a Path. The parts may be PathElements, ints, strings.
func MakePath(parts ...interface{}) (Path, error) {
var fp Path
for _, p := range parts {
switch t := p.(type) {
case PathElement:
fp = append(fp, t)
case int:
// TODO: Understand schema and object and convert this to the
// FieldSpecifier below if appropriate.
fp = append(fp, PathElement{Index: &t})
case string:
fp = append(fp, PathElement{FieldName: &t})
case []value.Field:
if len(t) == 0 {
return nil, fmt.Errorf("associative list key type path elements must have at least one key (got zero)")
}
fp = append(fp, PathElement{Key: t})
case value.Value:
// TODO: understand schema and verify that this is a set type
// TODO: make a copy of t
fp = append(fp, PathElement{Value: &t})
default:
return nil, fmt.Errorf("unable to make %#v into a path element", p)
}
}
return fp, nil
}
// MakePathOrDie panics if parts can't be turned into a path. Good for things
// that are known at complie time.
func MakePathOrDie(parts ...interface{}) Path {
fp, err := MakePath(parts...)
if err != nil {
panic(err)
}
return fp
}
/*
Copyright 2018 The Kubernetes 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 fieldpath
import (
"strings"
)
// Set identifies a set of fields.
type Set struct {
// Members lists fields that are part of the set.
// TODO: will be serialized as a list of path elements.
Members PathElementSet
// Children lists child fields which themselves have children that are
// members of the set. Appearance in this list does not imply membership.
// Note: this is a tree, not an arbitrary graph.
Children SetNodeMap
}
// NewSet makes a set from a list of paths.
func NewSet(paths ...Path) *Set {
s := &Set{}
for _, p := range paths {
s.Insert(p)
}
return s
}
// Insert adds the field identified by `p` to the set. Important: parent fields
// are NOT added to the set; if that is desired, they must be added separately.
func (s *Set) Insert(p Path) {
if len(p) == 0 {
// Zero-length path identifies the entire object; we don't
// track top-level ownership.
return
}
for {
if len(p) == 1 {
s.Members.Insert(p[0])
return
}
s = s.Children.Descend(p[0])
p = p[1:]
}
}
// Union returns a Set containing elements which appear in either s or s2.
func (s *Set) Union(s2 *Set) *Set {
return &Set{
Members: *s.Members.Union(&s2.Members),
Children: *s.Children.Union(&s2.Children),
}
}
// Intersection returns a Set containing leaf elements which appear in both s
// and s2. Intersection can be constructed from Union and Difference operations
// (example in the tests) but it's much faster to do it in one pass.
func (s *Set) Intersection(s2 *Set) *Set {
return &Set{
Members: *s.Members.Intersection(&s2.Members),
Children: *s.Children.Intersection(&s2.Children),
}
}
// Difference returns a Set containing elements which:
// * appear in s
// * do not appear in s2
// * and are not children of elements that appear in s2.
//
// In other words, for leaf fields, this acts like a regular set difference
// operation. When non leaf fields are compared with leaf fields ("parents"
// which contain "children"), the effect is:
// * parent - child = parent
// * child - parent = {empty set}
func (s *Set) Difference(s2 *Set) *Set {
return &Set{
Members: *s.Members.Difference(&s2.Members),
Children: *s.Children.Difference(s2),
}
}
// Size returns the number of members of the set.
func (s *Set) Size() int {
return s.Members.Size() + s.Children.Size()
}
// Empty returns true if there are no members of the set. It is a separate
// function from Size since it's common to check whether size > 0, and
// potentially much faster to return as soon as a single element is found.
func (s *Set) Empty() bool {
if s.Members.Size() > 0 {
return false
}
return s.Children.Empty()
}
// Has returns true if the field referenced by `p` is a member of the set.
func (s *Set) Has(p Path) bool {
if len(p) == 0 {
// No one owns "the entire object"
return false
}
for {
if len(p) == 1 {
return s.Members.Has(p[0])
}
var ok bool
s, ok = s.Children.Get(p[0])
if !ok {
return false
}
p = p[1:]
}
}
// Equals returns true if s and s2 have exactly the same members.
func (s *Set) Equals(s2 *Set) bool {
return s.Members.Equals(&s2.Members) && s.Children.Equals(&s2.Children)
}
// String returns the set one element per line.
func (s *Set) String() string {
elements := []string{}
s.Iterate(func(p Path) {
elements = append(elements, p.String())
})
return strings.Join(elements, "\n")
}
// Iterate calls f once for each field that is a member of the set (preorder
// DFS). The path passed to f will be reused so make a copy if you wish to keep
// it.
func (s *Set) Iterate(f func(Path)) {
s.iteratePrefix(Path{}, f)
}
func (s *Set) iteratePrefix(prefix Path, f func(Path)) {
s.Members.Iterate(func(pe PathElement) { f(append(prefix, pe)) })
s.Children.iteratePrefix(prefix, f)
}
// setNode is a pair of PathElement / Set, for the purpose of expressing
// nested set membership.
type setNode struct {
pathElement PathElement
set *Set
}
// SetNodeMap is a map of PathElement to subset.
type SetNodeMap struct {
members map[string]setNode
}
// Descend adds pe to the set if necessary, returning the associated subset.
func (s *SetNodeMap) Descend(pe PathElement) *Set {
serialized := pe.String()
if s.members == nil {
s.members = map[string]setNode{}
}
if n, ok := s.members[serialized]; ok {
return n.set
}
ss := &Set{}
s.members[serialized] = setNode{
pathElement: pe,
set: ss,
}
return ss
}
// Size returns the sum of the number of members of all subsets.
func (s *SetNodeMap) Size() int {
count := 0
for _, v := range s.members {
count += v.set.Size()
}
return count
}
// Empty returns false if there's at least one member in some child set.
func (s *SetNodeMap) Empty() bool {
for _, n := range s.members {
if !n.set.Empty() {
return false
}
}
return true
}
// Get returns (the associated set, true) or (nil, false) if there is none.
func (s *SetNodeMap) Get(pe PathElement) (*Set, bool) {
if s.members == nil {
return nil, false
}
serialized := pe.String()
if n, ok := s.members[serialized]; ok {
return n.set, true
}
return nil, false
}
// Equals returns true if s and s2 have the same structure (same nested
// child sets).
func (s *SetNodeMap) Equals(s2 *SetNodeMap) bool {
if len(s.members) != len(s2.members) {
return false
}
for k, v := range s.members {
v2, ok := s2.members[k]
if !ok {
return false
}
if !v.set.Equals(v2.set) {
return false
}
}
return true
}
// Union returns a SetNodeMap with members that appear in either s or s2.
func (s *SetNodeMap) Union(s2 *SetNodeMap) *SetNodeMap {
out := &SetNodeMap{}
for k, sn := range s.members {
pe := sn.pathElement
if sn2, ok := s2.members[k]; ok {
*out.Descend(pe) = *sn.set.Union(sn2.set)
} else {
*out.Descend(pe) = *sn.set
}
}
for k, sn2 := range s2.members {
pe := sn2.pathElement
if _, ok := s.members[k]; ok {
// already handled
continue
}
*out.Descend(pe) = *sn2.set
}
return out
}
// Intersection returns a SetNodeMap with members that appear in both s and s2.
func (s *SetNodeMap) Intersection(s2 *SetNodeMap) *SetNodeMap {
out := &SetNodeMap{}
for k, sn := range s.members {
pe := sn.pathElement
if sn2, ok := s2.members[k]; ok {
i := *sn.set.Intersection(sn2.set)
if !i.Empty() {
*out.Descend(pe) = i
}
}
}
return out
}
// Difference returns a SetNodeMap with members that appear in s but not in s2.
func (s *SetNodeMap) Difference(s2 *Set) *SetNodeMap {
out := &SetNodeMap{}
for k, sn := range s.members {
pe := sn.pathElement
if s2.Members.Has(pe) {
continue
}
if sn2, ok := s2.Children.members[k]; ok {
diff := *sn.set.Difference(sn2.set)
// We aren't permitted to add nodes with no elements.
if !diff.Empty() {
*out.Descend(pe) = diff
}
} else {
*out.Descend(pe) = *sn.set
}
}
return out
}
// Iterate calls f for each PathElement in the set.
func (s *SetNodeMap) Iterate(f func(PathElement)) {
for _, n := range s.members {
f(n.pathElement)
}
}
func (s *SetNodeMap) iteratePrefix(prefix Path, f func(Path)) {
for _, n := range s.members {
pe := n.pathElement
n.set.iteratePrefix(append(prefix, pe), f)
}
}
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"conflict.go",
"update.go",
],
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/merge",
importpath = "sigs.k8s.io/structured-merge-diff/merge",
visibility = ["//visibility:public"],
deps = [
"//vendor/sigs.k8s.io/structured-merge-diff/fieldpath:go_default_library",
"//vendor/sigs.k8s.io/structured-merge-diff/typed:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
Copyright 2018 The Kubernetes 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 merge
import (
"fmt"
"sort"
"strings"
"sigs.k8s.io/structured-merge-diff/fieldpath"
)
// Conflict is a conflict on a specific field with the current manager of
// that field. It does implement the error interface so that it can be
// used as an error.
type Conflict struct {
Manager string
Path fieldpath.Path
}
// Conflict is an error.
var _ error = Conflict{}
// Error formats the conflict as an error.
func (c Conflict) Error() string {
return fmt.Sprintf("conflict with %q: %v", c.Manager, c.Path)
}
// Conflicts accumulates multiple conflicts and aggregates them by managers.
type Conflicts []Conflict
var _ error = Conflicts{}
// Error prints the list of conflicts, grouped by sorted managers.
func (conflicts Conflicts) Error() string {
if len(conflicts) == 1 {
return conflicts[0].Error()
}
m := map[string][]fieldpath.Path{}
for _, conflict := range conflicts {
m[conflict.Manager] = append(m[conflict.Manager], conflict.Path)
}
managers := []string{}
for manager := range m {
managers = append(managers, manager)
}
// Print conflicts by sorted managers.
sort.Strings(managers)
messages := []string{}
for _, manager := range managers {
messages = append(messages, fmt.Sprintf("conflicts with %q:", manager))
for _, path := range m[manager] {
messages = append(messages, fmt.Sprintf("- %v", path))
}
}
return strings.Join(messages, "\n")
}
// ConflictsFromManagers creates a list of conflicts given Managers sets.
func ConflictsFromManagers(sets fieldpath.ManagedFields) Conflicts {
conflicts := []Conflict{}
for manager, set := range sets {
set.Iterate(func(p fieldpath.Path) {
conflicts = append(conflicts, Conflict{
Manager: manager,
Path: p,
})
})
}
return conflicts
}
/*
Copyright 2018 The Kubernetes 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 merge
import (
"fmt"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/typed"
)
// Converter is an interface to the conversion logic. The converter
// needs to be able to convert objects from one version to another.
type Converter interface {
Convert(object typed.TypedValue, version fieldpath.APIVersion) (typed.TypedValue, error)
}
// Updater is the object used to compute updated FieldSets and also
// merge the object on Apply.
type Updater struct {
Converter Converter
}
func (s *Updater) update(oldObject, newObject typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, workflow string, force bool) (fieldpath.ManagedFields, error) {
if managers == nil {
managers = fieldpath.ManagedFields{}
}
conflicts := fieldpath.ManagedFields{}
type Versioned struct {
oldObject typed.TypedValue
newObject typed.TypedValue
}
versions := map[fieldpath.APIVersion]Versioned{
version: Versioned{
oldObject: oldObject,
newObject: newObject,
},
}
for manager, managerSet := range managers {
if manager == workflow {
continue
}
versioned, ok := versions[managerSet.APIVersion]
if !ok {
var err error
versioned.oldObject, err = s.Converter.Convert(oldObject, managerSet.APIVersion)
if err != nil {
return nil, fmt.Errorf("failed to convert old object: %v", err)
}
versioned.newObject, err = s.Converter.Convert(newObject, managerSet.APIVersion)
if err != nil {
return nil, fmt.Errorf("failed to convert new object: %v", err)
}
versions[managerSet.APIVersion] = versioned
}
compare, err := versioned.oldObject.Compare(versioned.newObject)
if err != nil {
return nil, fmt.Errorf("failed to compare objects: %v", err)
}
conflictSet := managerSet.Intersection(compare.Modified.Union(compare.Added))
if !conflictSet.Empty() {
conflicts[manager] = &fieldpath.VersionedSet{
Set: conflictSet,
APIVersion: managerSet.APIVersion,
}
}
}
if !force && len(conflicts) != 0 {
return nil, ConflictsFromManagers(conflicts)
}
for manager, conflictSet := range conflicts {
managers[manager].Set = managers[manager].Set.Difference(conflictSet.Set)
}
return managers, nil
}
// Update is the method you should call once you've merged your final
// object on CREATE/UPDATE/PATCH verbs. newObject must be the object
// that you intend to persist (after applying the patch if this is for a
// PATCH call), and liveObject must be the original object (empty if
// this is a CREATE call).
func (s *Updater) Update(liveObject, newObject typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, manager string) (fieldpath.ManagedFields, error) {
var err error
managers, err = s.update(liveObject, newObject, version, managers, manager, true)
if err != nil {
return fieldpath.ManagedFields{}, err
}
compare, err := liveObject.Compare(newObject)
if err != nil {
return fieldpath.ManagedFields{}, fmt.Errorf("failed to compare live and new objects: %v", err)
}
if _, ok := managers[manager]; !ok {
managers[manager] = &fieldpath.VersionedSet{
Set: fieldpath.NewSet(),
}
}
managers[manager].Set = managers[manager].Set.Union(compare.Modified).Union(compare.Added).Difference(compare.Removed)
managers[manager].APIVersion = version
return managers, nil
}
// Apply should be called when Apply is run, given the current object as
// well as the configuration that is applied. This will merge the object
// and return it.
func (s *Updater) Apply(liveObject, configObject typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, manager string, force bool) (typed.TypedValue, fieldpath.ManagedFields, error) {
newObject, err := liveObject.Merge(configObject)
if err != nil {
return typed.TypedValue{}, fieldpath.ManagedFields{}, fmt.Errorf("failed to merge config: %v", err)
}
managers, err = s.update(liveObject, newObject, version, managers, manager, force)
if err != nil {
return typed.TypedValue{}, fieldpath.ManagedFields{}, err
}
// TODO: Remove unconflicting removed fields
set, err := configObject.ToFieldSet()
if err != nil {
return typed.TypedValue{}, fieldpath.ManagedFields{}, fmt.Errorf("failed to get field set: %v", err)
}
managers[manager] = &fieldpath.VersionedSet{
Set: set,
APIVersion: version,
}
return newObject, managers, nil
}
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"elements.go",
"schemaschema.go",
],
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/schema",
importpath = "sigs.k8s.io/structured-merge-diff/schema",
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
Copyright 2018 The Kubernetes 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 schema defines a targeted schema language which allows one to
// represent all the schema information necessary to perform "structured"
// merges and diffs.
//
// Due to the targeted nature of the data model, the schema language can fit in
// just a few hundred lines of go code, making it much more understandable and
// concise than e.g. OpenAPI.
//
// This schema was derived by observing the API objects used by Kubernetes, and
// formalizing a model which allows certain operations ("apply") to be more
// well defined. It is currently missing one feature: one-of ("unions").
package schema
/*
Copyright 2018 The Kubernetes 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 schema
// Schema is a list of named types.
type Schema struct {
Types []TypeDef `yaml:"types,omitempty"`
}
// A TypeSpecifier references a particular type in a schema.
type TypeSpecifier struct {
Type TypeRef `yaml:"type,omitempty"`
Schema Schema `yaml:"schema,omitempty"`
}
// TypeDef represents a named type in a schema.
type TypeDef struct {
// Top level types should be named. Every type must have a unique name.
Name string `yaml:"name,omitempty"`
Atom `yaml:"atom,omitempty,inline"`
}
// TypeRef either refers to a named type or declares an inlined type.
type TypeRef struct {
// Either the name or one member of Atom should be set.
NamedType *string `yaml:"namedType,omitempty"`
Inlined Atom `yaml:",inline,omitempty"`
}
// Atom represents the smallest possible pieces of the type system.
type Atom struct {
// Exactly one of the below must be set.
*Scalar `yaml:"scalar,omitempty"`
*Struct `yaml:"struct,omitempty"`
*List `yaml:"list,omitempty"`
*Map `yaml:"map,omitempty"`
*Untyped `yaml:"untyped,omitempty"`
}
// Scalar (AKA "primitive") represents a type which has a single value which is
// either numeric, string, or boolean.
//
// TODO: split numeric into float/int? Something even more fine-grained?
type Scalar string
const (
Numeric = Scalar("numeric")
String = Scalar("string")
Boolean = Scalar("boolean")
)
// ElementRelationship is an enum of the different possible relationships
// between the elements of container types (maps, lists, structs, untyped).
type ElementRelationship string
const (
// Associative only applies to lists (see the documentation there).
Associative = ElementRelationship("associative")
// Atomic makes container types (lists, maps, structs, untyped) behave
// as scalars / leaf fields (which is the default for untyped data).
Atomic = ElementRelationship("atomic")
// Separable means the items of the container type have no particular
// relationship (default behavior for maps and structs).
Separable = ElementRelationship("separable")
)
// Struct represents a type which is composed of a number of different fields.
// Each field has a name and a type.
//
// TODO: in the future, we will add one-of groups (sometimes called unions).
type Struct struct {
// Each struct field appears exactly once in this list. The order in
// this list defines the canonical field ordering.
Fields []StructField `yaml:"fields,omitempty"`
// TODO: Implement unions, either this way or by inlining.
// Unions are groupings of fields with special rules. They may refer to
// one or more fields in the above list. A given field from the above
// list may be referenced in exactly 0 or 1 places in the below list.
// Unions []Union `yaml:"unions,omitempty"`
// ElementRelationship states the relationship between the struct's items.
// * `separable` (or unset) implies that each element is 100% independent.
// * `atomic` implies that all elements depend on each other, and this
// is effectively a scalar / leaf field; it doesn't make sense for
// separate actors to set the elements. Example: an RGB color struct;
// it would never make sense to "own" only one component of the
// color.
// The default behavior for structs is `separable`; it's permitted to
// leave this unset to get the default behavior.
ElementRelationship ElementRelationship `yaml:"elementRelationship,omitempty"`
}
// StructField pairs a field name with a field type.
type StructField struct {
// Name is the field name.
Name string `yaml:"name,omitempty"`
// Type is the field type.
Type TypeRef `yaml:"type,omitempty"`
}
// List represents a type which contains a zero or more elements, all of the
// same subtype. Lists may be either associative: each element is more or less
// independent and could be managed by separate entities in the system; or
// atomic, where the elements are heavily dependent on each other: it is not
// sensible to change one element without considering the ramifications on all
// the other elements.
type List struct {
// ElementType is the type of the list's elements.
ElementType TypeRef `yaml:"elementType,omitempty"`
// ElementRelationship states the relationship between the list's elements
// and must have one of these values:
// * `atomic`: the list is treated as a single entity, like a scalar.
// * `associative`:
// - If the list element is a scalar, the list is treated as a set.
// - If the list element is a struct, the list is treated as a map.
// - The list element must not be a map or a list itself.
// There is no default for this value for lists; all schemas must
// explicitly state the element relationship for all lists.
ElementRelationship ElementRelationship `yaml:"elementRelationship,omitempty"`
// Iff ElementRelationship is `associative`, and the element type is
// struct, then Keys must have non-zero length, and it lists the fields
// of the element's struct type which are to be used as the keys of the
// list.
//
// TODO: change this to "non-atomic struct" above and make the code reflect this.
//
// Each key must refer to a single field name (no nesting, not JSONPath).
Keys []string `yaml:"keys,omitempty"`
}
// Map is a key-value pair. Its default semantics are the same as an
// associative list, but:
// * It is serialized differently:
// map: {"k": {"value": "v"}}
// list: [{"key": "k", "value": "v"}]
// * Keys must be string typed.
// * Keys can't have multiple components.
//
// Although serialized the same, maps are different from structs in that each
// map item must have the same type.
//
// Optionally, maps may be atomic (for example, imagine representing an RGB
// color value--it doesn't make sense to have different actors own the R and G
// values).
type Map struct {
// ElementType is the type of the list's elements.
ElementType TypeRef `yaml:"elementType,omitempty"`
// ElementRelationship states the relationship between the map's items.
// * `separable` implies that each element is 100% independent.
// * `atomic` implies that all elements depend on each other, and this
// is effectively a scalar / leaf field; it doesn't make sense for
// separate actors to set the elements.
// TODO: find a simple example.
// The default behavior for maps is `separable`; it's permitted to
// leave this unset to get the default behavior.
ElementRelationship ElementRelationship `yaml:"elementRelationship,omitempty"`
}
// Untyped represents types that allow arbitrary content. (Think: plugin
// objects.)
type Untyped struct {
// ElementRelationship states the relationship between the items, if
// container-typed data happens to be present here.
// * `atomic` implies that all elements depend on each other, and this
// is effectively a scalar / leaf field; it doesn't make sense for
// separate actors to set the elements.
// TODO: support "guess" (guesses at associative list keys)
// TODO: support "lookup" (calls a lookup function to figure out the
// schema based on the data)
// The default behavior for untyped data is `atomic`; it's permitted to
// leave this unset to get the default behavior.
ElementRelationship ElementRelationship `yaml:"elementRelationship,omitempty"`
}
// FindNamedType is a convenience function that returns the referenced TypeDef,
// if it exists, or (nil, false) if it doesn't.
func (s Schema) FindNamedType(name string) (TypeDef, bool) {
for _, t := range s.Types {
if t.Name == name {
return t, true
}
}
return TypeDef{}, false
}
// Resolve is a convenience function which returns the atom referenced, whether
// it is inline or named. Returns (Atom{}, false) if the type can't be resolved.
//
// This allows callers to not care about the difference between a (possibly
// inlined) reference and a definition.
func (s Schema) Resolve(tr TypeRef) (Atom, bool) {
if tr.NamedType != nil {
t, ok := s.FindNamedType(*tr.NamedType)
if !ok {
return Atom{}, false
}
return t.Atom, true
}
return tr.Inlined, true
}
/*
Copyright 2018 The Kubernetes 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 schema
// SchemaSchemaYAML is a schema against which you can validate other schemas.
// It will validate itself. It can be unmarshalled into a Schema type.
var SchemaSchemaYAML = `types:
- name: schema
struct:
fields:
- name: types
type:
list:
elementRelationship: associative
elementType:
namedType: typeDef
keys:
- name
- name: typeDef
struct:
fields:
- name: name
type:
scalar: string
- name: scalar
type:
scalar: string
- name: struct
type:
namedType: struct
- name: list
type:
namedType: list
- name: map
type:
namedType: map
- name: untyped
type:
namedType: untyped
- name: typeRef
struct:
fields:
- name: namedType
type:
scalar: string
- name: scalar
type:
scalar: string
- name: struct
type:
namedType: struct
- name: list
type:
namedType: list
- name: map
type:
namedType: map
- name: untyped
type:
namedType: untyped
- name: scalar
scalar: string
- name: struct
struct:
fields:
- name: fields
type:
list:
elementType:
namedType: structField
elementRelationship: associative
keys: [ "name" ]
- name: elementRelationship
type:
scalar: string
- name: structField
struct:
fields:
- name: name
type:
scalar: string
- name: type
type:
namedType: typeRef
- name: list
struct:
fields:
- name: elementType
type:
namedType: typeRef
- name: elementRelationship
type:
scalar: string
- name: keys
type:
list:
elementType:
scalar: string
- name: map
struct:
fields:
- name: elementType
type:
namedType: typeRef
- name: elementRelationship
type:
scalar: string
- name: untyped
struct:
fields:
- name: elementRelationship
type:
scalar: string
`
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"helpers.go",
"merge.go",
"parser.go",
"typed.go",
"validate.go",
],
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/typed",
importpath = "sigs.k8s.io/structured-merge-diff/typed",
visibility = ["//visibility:public"],
deps = [
"//vendor/gopkg.in/yaml.v2:go_default_library",
"//vendor/sigs.k8s.io/structured-merge-diff/fieldpath:go_default_library",
"//vendor/sigs.k8s.io/structured-merge-diff/schema:go_default_library",
"//vendor/sigs.k8s.io/structured-merge-diff/value:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
Copyright 2018 The Kubernetes 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 typed contains logic for operating on values with given schemas.
package typed
/*
Copyright 2018 The Kubernetes 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 typed
import (
"errors"
"fmt"
"strings"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/schema"
"sigs.k8s.io/structured-merge-diff/value"
)
// ValidationError reports an error about a particular field
type ValidationError struct {
Path fieldpath.Path
ErrorMessage string
}
// Error returns a human readable error message.
func (ve ValidationError) Error() string {
if len(ve.Path) == 0 {
return ve.ErrorMessage
}
return fmt.Sprintf("%s: %v", ve.Path, ve.ErrorMessage)
}
// ValidationErrors accumulates multiple validation error messages.
type ValidationErrors []ValidationError
// Error returns a human readable error message reporting each error in the
// list.
func (errs ValidationErrors) Error() string {
if len(errs) == 1 {
return errs[0].Error()
}
messages := []string{"errors:"}
for _, e := range errs {
messages = append(messages, " "+e.Error())
}
return strings.Join(messages, "\n")
}
// errorFormatter makes it easy to keep a list of validation errors. They
// should all be packed into a single error object before leaving the package
// boundary, since it's weird to have functions not return a plain error type.
type errorFormatter struct {
path fieldpath.Path
}
func (ef *errorFormatter) descend(pe fieldpath.PathElement) {
ef.path = append(ef.path, pe)
}
func (ef errorFormatter) errorf(format string, args ...interface{}) ValidationErrors {
return ValidationErrors{{
Path: append(fieldpath.Path{}, ef.path...),
ErrorMessage: fmt.Sprintf(format, args...),
}}
}
func (ef errorFormatter) error(err error) ValidationErrors {
return ValidationErrors{{
Path: append(fieldpath.Path{}, ef.path...),
ErrorMessage: err.Error(),
}}
}
func (ef errorFormatter) prefixError(prefix string, err error) ValidationErrors {
return ValidationErrors{{
Path: append(fieldpath.Path{}, ef.path...),
ErrorMessage: prefix + err.Error(),
}}
}
type atomHandler interface {
doScalar(schema.Scalar) ValidationErrors
doStruct(schema.Struct) ValidationErrors
doList(schema.List) ValidationErrors
doMap(schema.Map) ValidationErrors
doUntyped(schema.Untyped) ValidationErrors
errorf(msg string, args ...interface{}) ValidationErrors
}
func resolveSchema(s *schema.Schema, tr schema.TypeRef, ah atomHandler) ValidationErrors {
a, ok := s.Resolve(tr)
if !ok {
return ah.errorf("schema error: no type found matching: %v", *tr.NamedType)
}
switch {
case a.Scalar != nil:
return ah.doScalar(*a.Scalar)
case a.Struct != nil:
return ah.doStruct(*a.Struct)
case a.List != nil:
return ah.doList(*a.List)
case a.Map != nil:
return ah.doMap(*a.Map)
case a.Untyped != nil:
return ah.doUntyped(*a.Untyped)
}
name := "inlined"
if tr.NamedType != nil {
name = "named type: " + *tr.NamedType
}
return ah.errorf("schema error: invalid atom: %v", name)
}
func (ef errorFormatter) validateScalar(t schema.Scalar, v *value.Value, prefix string) (errs ValidationErrors) {
if v == nil {
return nil
}
switch t {
case schema.Numeric:
if v.FloatValue == nil && v.IntValue == nil {
// TODO: should the schema separate int and float?
return ef.errorf("%vexpected numeric (int or float), got %v", prefix, v)
}
case schema.String:
if v.StringValue == nil {
return ef.errorf("%vexpected string, got %v", prefix, v)
}
case schema.Boolean:
if v.BooleanValue == nil {
return ef.errorf("%vexpected boolean, got %v", prefix, v)
}
}
return nil
}
// Returns the list, or an error. Reminder: nil is a valid list and might be returned.
func listValue(val value.Value) (*value.List, error) {
switch {
case val.Null:
// Null is a valid list.
return nil, nil
case val.ListValue != nil:
return val.ListValue, nil
default:
return nil, fmt.Errorf("expected list, got %v", val)
}
}
// Returns the map, or an error. Reminder: nil is a valid map and might be returned.
func mapOrStructValue(val value.Value, typeName string) (*value.Map, error) {
switch {
case val.Null:
return nil, nil
case val.MapValue != nil:
return val.MapValue, nil
default:
return nil, fmt.Errorf("expected %v, got %v", typeName, val)
}
}
func (ef errorFormatter) rejectExtraStructFields(m *value.Map, allowedNames map[string]struct{}, prefix string) (errs ValidationErrors) {
if m == nil {
return nil
}
for _, f := range m.Items {
if _, allowed := allowedNames[f.Name]; !allowed {
errs = append(errs, ef.errorf("%vfield %v is not mentioned in the schema", prefix, f.Name)...)
}
}
return errs
}
func keyedAssociativeListItemToPathElement(list schema.List, index int, child value.Value) (fieldpath.PathElement, error) {
pe := fieldpath.PathElement{}
if child.Null {
// For now, the keys are required which means that null entries
// are illegal.
return pe, errors.New("associative list with keys may not have a null element")
}
if child.MapValue == nil {
return pe, errors.New("associative list with keys may not have non-map elements")
}
for _, fieldName := range list.Keys {
var fieldValue value.Value
field, ok := child.MapValue.Get(fieldName)
if ok {
fieldValue = field.Value
} else {
// Treat keys as required.
return pe, fmt.Errorf("associative list with keys has an element that omits key field %q", fieldName)
}
pe.Key = append(pe.Key, value.Field{
Name: fieldName,
Value: fieldValue,
})
}
return pe, nil
}
func setItemToPathElement(list schema.List, index int, child value.Value) (fieldpath.PathElement, error) {
pe := fieldpath.PathElement{}
switch {
case child.MapValue != nil:
// TODO: atomic maps should be acceptable.
return pe, errors.New("associative list without keys has an element that's a map type")
case child.ListValue != nil:
// Should we support a set of lists? For the moment
// let's say we don't.
// TODO: atomic lists should be acceptable.
return pe, errors.New("not supported: associative list with lists as elements")
case child.Null:
return pe, errors.New("associative list without keys has an element that's an explicit null")
default:
// We are a set type.
pe.Value = &child
return pe, nil
}
}
func listItemToPathElement(list schema.List, index int, child value.Value) (fieldpath.PathElement, error) {
if list.ElementRelationship == schema.Associative {
if len(list.Keys) > 0 {
return keyedAssociativeListItemToPathElement(list, index, child)
}
// If there's no keys, then we must be a set of primitives.
return setItemToPathElement(list, index, child)
}
// Use the index as a key for atomic lists.
return fieldpath.PathElement{Index: &index}, nil
}
/*
Copyright 2018 The Kubernetes 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 typed
import (
"fmt"
yaml "gopkg.in/yaml.v2"
"sigs.k8s.io/structured-merge-diff/schema"
"sigs.k8s.io/structured-merge-diff/value"
)
// YAMLObject is an object encoded in YAML.
type YAMLObject string
// Parser implements YAMLParser and allows introspecting the schema.
type Parser struct {
Schema schema.Schema
}
// create builds an unvalidated parser.
func create(schema YAMLObject) (*Parser, error) {
p := Parser{}
err := yaml.Unmarshal([]byte(schema), &p.Schema)
return &p, err
}
func createOrDie(schema YAMLObject) *Parser {
p, err := create(schema)
if err != nil {
panic(fmt.Errorf("failed to create parser: %v", err))
}
return p
}
var ssParser = createOrDie(YAMLObject(schema.SchemaSchemaYAML))
// NewParser will build a YAMLParser from a schema. The schema is validated.
func NewParser(schema YAMLObject) (*Parser, error) {
_, err := ssParser.Type("schema").FromYAML(schema)
if err != nil {
return nil, fmt.Errorf("unable to validate schema: %v", err)
}
return create(schema)
}
// TypeNames returns a list of types this parser understands.
func (p *Parser) TypeNames() (names []string) {
for _, td := range p.Schema.Types {
names = append(names, td.Name)
}
return names
}
// Type returns a helper which can produce objects of the given type. Any
// errors are deferred until a further function is called.
func (p *Parser) Type(name string) *ParseableType {
return &ParseableType{
parser: p,
typename: name,
}
}
// ParseableType allows for easy production of typed objects.
type ParseableType struct {
parser *Parser
typename string
}
// IsValid return true if p's schema and typename are valid.
func (p *ParseableType) IsValid() bool {
_, ok := p.parser.Schema.Resolve(schema.TypeRef{NamedType: &p.typename})
return ok
}
// New returns a new empty object with the current schema and the
// type "typename".
func (p *ParseableType) New() (TypedValue, error) {
return p.FromYAML(YAMLObject("{}"))
}
// FromYAML parses a yaml string into an object with the current schema
// and the type "typename" or an error if validation fails.
func (p *ParseableType) FromYAML(object YAMLObject) (TypedValue, error) {
v, err := value.FromYAML([]byte(object))
if err != nil {
return TypedValue{}, err
}
return AsTyped(v, &p.parser.Schema, p.typename)
}
// FromUnstructured converts a go interface to a TypedValue. It will return an
// error if the resulting object fails schema validation.
func (p *ParseableType) FromUnstructured(in interface{}) (TypedValue, error) {
v, err := value.FromUnstructured(in)
if err != nil {
return TypedValue{}, err
}
return AsTyped(v, &p.parser.Schema, p.typename)
}
/*
Copyright 2018 The Kubernetes 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 typed
import (
"fmt"
"reflect"
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/schema"
"sigs.k8s.io/structured-merge-diff/value"
)
// TypedValue is a value of some specific type.
type TypedValue struct {
value value.Value
typeRef schema.TypeRef
schema *schema.Schema
}
// AsTyped accepts a value and a type and returns a TypedValue. 'v' must have
// type 'typeName' in the schema. An error is returned if the v doesn't conform
// to the schema.
func AsTyped(v value.Value, s *schema.Schema, typeName string) (TypedValue, error) {
tv := TypedValue{
value: v,
typeRef: schema.TypeRef{NamedType: &typeName},
schema: s,
}
if err := tv.Validate(); err != nil {
return TypedValue{}, err
}
return tv, nil
}
// AsValue removes the type from the TypedValue and only keeps the value.
func (tv TypedValue) AsValue() *value.Value {
return &tv.value
}
// Validate returns an error with a list of every spec violation.
func (tv TypedValue) Validate() error {
if errs := tv.walker().validate(); len(errs) != 0 {
return errs
}
return nil
}
// ToFieldSet creates a set containing every leaf field mentioned in tv, or
// validation errors, if any were encountered.
func (tv TypedValue) ToFieldSet() (*fieldpath.Set, error) {
s := fieldpath.NewSet()
w := tv.walker()
w.leafFieldCallback = func(p fieldpath.Path) { s.Insert(p) }
if errs := w.validate(); len(errs) != 0 {
return nil, errs
}
return s, nil
}
// Merge returns the result of merging tv and pso ("partially specified
// object") together. Of note:
// * No fields can be removed by this operation.
// * If both tv and pso specify a given leaf field, the result will keep pso's
// value.
// * Container typed elements will have their items ordered:
// * like tv, if pso doesn't change anything in the container
// * like pso, if pso does change something in the container.
// tv and pso must both be of the same type (their Schema and TypeRef must
// match), or an error will be returned. Validation errors will be returned if
// the objects don't conform to the schema.
func (tv TypedValue) Merge(pso TypedValue) (TypedValue, error) {
return merge(tv, pso, ruleKeepRHS, nil)
}
// Comparison is the return value of a TypedValue.Compare() operation.
//
// No field will appear in more than one of the three fieldsets. If all of the
// fieldsets are empty, then the objects must have been equal.
type Comparison struct {
// Merged is the result of merging the two objects, as explained in the
// comments on TypedValue.Merge().
Merged TypedValue
// Removed contains any fields removed by rhs (the right-hand-side
// object in the comparison).
Removed *fieldpath.Set
// Modified contains fields present in both objects but different.
Modified *fieldpath.Set
// Added contains any fields added by rhs.
Added *fieldpath.Set
}
// IsSame returns true if the comparison returned no changes (the two
// compared objects are similar).
func (c *Comparison) IsSame() bool {
return c.Removed.Empty() && c.Modified.Empty() && c.Added.Empty()
}
// String returns a human readable version of the comparison.
func (c *Comparison) String() string {
str := fmt.Sprintf("- Merged Object:\n%v\n", c.Merged.AsValue())
if !c.Modified.Empty() {
str += fmt.Sprintf("- Modified Fields:\n%v\n", c.Modified)
}
if !c.Added.Empty() {
str += fmt.Sprintf("- Added Fields:\n%v\n", c.Added)
}
if !c.Removed.Empty() {
str += fmt.Sprintf("- Removed Fields:\n%v\n", c.Removed)
}
return str
}
// Compare compares the two objects. See the comments on the `Comparison`
// struct for details on the return value.
//
// tv and rhs must both be of the same type (their Schema and TypeRef must
// match), or an error will be returned. Validation errors will be returned if
// the objects don't conform to the schema.
func (tv TypedValue) Compare(rhs TypedValue) (c *Comparison, err error) {
c = &Comparison{
Removed: fieldpath.NewSet(),
Modified: fieldpath.NewSet(),
Added: fieldpath.NewSet(),
}
c.Merged, err = merge(tv, rhs, func(w *mergingWalker) {
if w.lhs == nil {
c.Added.Insert(w.path)
} else if w.rhs == nil {
c.Removed.Insert(w.path)
} else if !reflect.DeepEqual(w.rhs, w.lhs) {
// TODO: reflect.DeepEqual is not sufficient for this.
// Need to implement equality check on the value type.
c.Modified.Insert(w.path)
}
ruleKeepRHS(w)
}, func(w *mergingWalker) {
if w.lhs == nil {
c.Added.Insert(w.path)
} else if w.rhs == nil {
c.Removed.Insert(w.path)
}
})
if err != nil {
return nil, err
}
return c, nil
}
func merge(lhs, rhs TypedValue, rule, postRule mergeRule) (TypedValue, error) {
if lhs.schema != rhs.schema {
return TypedValue{}, errorFormatter{}.
errorf("expected objects with types from the same schema")
}
if !reflect.DeepEqual(lhs.typeRef, rhs.typeRef) {
return TypedValue{}, errorFormatter{}.
errorf("expected objects of the same type, but got %v and %v", lhs.typeRef, rhs.typeRef)
}
mw := mergingWalker{
lhs: &lhs.value,
rhs: &rhs.value,
schema: lhs.schema,
typeRef: lhs.typeRef,
rule: rule,
postItemHook: postRule,
}
errs := mw.merge()
if len(errs) > 0 {
return TypedValue{}, errs
}
out := TypedValue{
schema: lhs.schema,
typeRef: lhs.typeRef,
}
if mw.out == nil {
out.value = value.Value{Null: true}
} else {
out.value = *mw.out
}
return out, nil
}
// AsTypeUnvalidated is just like WithType, but doesn't validate that the type
// conforms to the schema, for cases where that has already been checked or
// where you're going to call a method that validates as a side-effect (like
// ToFieldSet).
func AsTypedUnvalidated(v value.Value, s *schema.Schema, typeName string) TypedValue {
tv := TypedValue{
value: v,
typeRef: schema.TypeRef{NamedType: &typeName},
schema: s,
}
return tv
}
/*
Copyright 2018 The Kubernetes 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 typed
import (
"sigs.k8s.io/structured-merge-diff/fieldpath"
"sigs.k8s.io/structured-merge-diff/schema"
"sigs.k8s.io/structured-merge-diff/value"
)
func (tv TypedValue) walker() *validatingObjectWalker {
return &validatingObjectWalker{
value: tv.value,
schema: tv.schema,
typeRef: tv.typeRef,
}
}
type validatingObjectWalker struct {
errorFormatter
value value.Value
schema *schema.Schema
typeRef schema.TypeRef
// If set, this is called on "leaf fields":
// * scalars: int/string/float/bool
// * atomic maps and lists
// * untyped fields
leafFieldCallback func(fieldpath.Path)
// internal housekeeping--don't set when constructing.
inLeaf bool // Set to true if we're in a "big leaf"--atomic map/list
}
func (v validatingObjectWalker) validate() ValidationErrors {
return resolveSchema(v.schema, v.typeRef, v)
}
// doLeaf should be called on leaves before descending into children, if there
// will be a descent. It modifies v.inLeaf.
func (v *validatingObjectWalker) doLeaf() {
if v.inLeaf {
// We're in a "big leaf", an atomic map or list. Ignore
// subsequent leaves.
return
}
v.inLeaf = true
if v.leafFieldCallback != nil {
// At the moment, this is only used to build fieldsets; we can
// add more than the path in here if needed.
v.leafFieldCallback(v.path)
}
}
func (v validatingObjectWalker) doScalar(t schema.Scalar) ValidationErrors {
if errs := v.validateScalar(t, &v.value, ""); len(errs) > 0 {
return errs
}
// All scalars are leaf fields.
v.doLeaf()
return nil
}
func (v validatingObjectWalker) visitStructFields(t schema.Struct, m *value.Map) (errs ValidationErrors) {
allowedNames := map[string]struct{}{}
for i := range t.Fields {
// I don't want to use the loop variable since a reference
// might outlive the loop iteration (in an error message).
f := t.Fields[i]
allowedNames[f.Name] = struct{}{}
child, ok := m.Get(f.Name)
if !ok {
// All fields are optional
continue
}
v2 := v
v2.errorFormatter.descend(fieldpath.PathElement{FieldName: &f.Name})
v2.value = child.Value
v2.typeRef = f.Type
errs = append(errs, v2.validate()...)
}
// All fields may be optional, but unknown fields are not allowed.
return append(errs, v.rejectExtraStructFields(m, allowedNames, "")...)
}
func (v validatingObjectWalker) doStruct(t schema.Struct) (errs ValidationErrors) {
m, err := mapOrStructValue(v.value, "struct")
if err != nil {
return v.error(err)
}
if t.ElementRelationship == schema.Atomic {
v.doLeaf()
}
if m == nil {
// nil is a valid map!
return nil
}
errs = v.visitStructFields(t, m)
// TODO: Check unions.
return errs
}
func (v validatingObjectWalker) visitListItems(t schema.List, list *value.List) (errs ValidationErrors) {
observedKeys := map[string]struct{}{}
for i, child := range list.Items {
pe, err := listItemToPathElement(t, i, child)
if err != nil {
errs = append(errs, v.errorf("element %v: %v", i, err.Error())...)
// If we can't construct the path element, we can't
// even report errors deeper in the schema, so bail on
// this element.
continue
}
keyStr := pe.String()
if _, found := observedKeys[keyStr]; found {
errs = append(errs, v.errorf("duplicate entries for key %v", keyStr)...)
}
observedKeys[keyStr] = struct{}{}
v2 := v
v2.errorFormatter.descend(pe)
v2.value = child
v2.typeRef = t.ElementType
errs = append(errs, v2.validate()...)
}
return errs
}
func (v validatingObjectWalker) doList(t schema.List) (errs ValidationErrors) {
list, err := listValue(v.value)
if err != nil {
return v.error(err)
}
if t.ElementRelationship == schema.Atomic {
v.doLeaf()
}
if list == nil {
return nil
}
errs = v.visitListItems(t, list)
return errs
}
func (v validatingObjectWalker) visitMapItems(t schema.Map, m *value.Map) (errs ValidationErrors) {
for _, item := range m.Items {
v2 := v
name := item.Name
v2.errorFormatter.descend(fieldpath.PathElement{FieldName: &name})
v2.value = item.Value
v2.typeRef = t.ElementType
errs = append(errs, v2.validate()...)
}
return errs
}
func (v validatingObjectWalker) doMap(t schema.Map) (errs ValidationErrors) {
m, err := mapOrStructValue(v.value, "map")
if err != nil {
return v.error(err)
}
if t.ElementRelationship == schema.Atomic {
v.doLeaf()
}
if m == nil {
return nil
}
errs = v.visitMapItems(t, m)
return errs
}
func (v validatingObjectWalker) doUntyped(t schema.Untyped) (errs ValidationErrors) {
if t.ElementRelationship == "" || t.ElementRelationship == schema.Atomic {
// Untyped sections allow anything, and are considered leaf
// fields.
v.doLeaf()
}
return nil
}
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"unstructured.go",
"value.go",
],
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/value",
importpath = "sigs.k8s.io/structured-merge-diff/value",
visibility = ["//visibility:public"],
deps = ["//vendor/gopkg.in/yaml.v2:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
Copyright 2018 The Kubernetes 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 value defines types for an in-memory representation of yaml or json
// objects, organized for convenient comparison with a schema (as defined by
// the sibling schema package). Functions for reading and writing the objects
// are also provided.
package value
/*
Copyright 2018 The Kubernetes 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 value
import (
"encoding/json"
"fmt"
"gopkg.in/yaml.v2"
)
// FromYAML is a helper function for reading a YAML document; it attempts to
// preserve order of keys within maps/structs. This is as a convenience to
// humans keeping YAML documents, not because there is a behavior difference.
//
// Known bug: objects with top-level arrays don't parse correctly.
func FromYAML(input []byte) (Value, error) {
var decoded interface{}
if len(input) == 0 || (len(input) == 4 && string(input) == "null") {
// Special case since the yaml package doesn't accurately
// preserve this.
return Value{Null: true}, nil
}
// This attempts to enable order sensitivity; note the yaml package is
// broken for documents that have root-level arrays, hence the two-step
// approach. TODO: This is a horrific hack. Is it worth it?
var ms yaml.MapSlice
if err := yaml.Unmarshal(input, &ms); err == nil {
decoded = ms
} else if err := yaml.Unmarshal(input, &decoded); err != nil {
return Value{}, err
}
v, err := FromUnstructured(decoded)
if err != nil {
return Value{}, fmt.Errorf("failed to interpret (%v):\n%s", err, input)
}
return v, nil
}
// FromJSON is a helper function for reading a JSON document
func FromJSON(input []byte) (Value, error) {
var decoded interface{}
if err := json.Unmarshal(input, &decoded); err != nil {
return Value{}, err
}
v, err := FromUnstructured(decoded)
if err != nil {
return Value{}, fmt.Errorf("failed to interpret (%v):\n%s", err, input)
}
return v, nil
}
// FromUnstructured will convert a go interface to a Value.
// It's most commonly expected to be used with map[string]interface{} as the
// input. `in` must not have any structures with cycles in them.
// yaml.MapSlice may be used for order-preservation.
func FromUnstructured(in interface{}) (Value, error) {
if in == nil {
return Value{Null: true}, nil
}
switch t := in.(type) {
case map[interface{}]interface{}:
m := Map{}
for rawKey, rawVal := range t {
k, ok := rawKey.(string)
if !ok {
return Value{}, fmt.Errorf("key %#v: not a string", k)
}
v, err := FromUnstructured(rawVal)
if err != nil {
return Value{}, fmt.Errorf("key %v: %v", k, err)
}
m.Set(k, v)
}
return Value{MapValue: &m}, nil
case map[string]interface{}:
m := Map{}
for k, rawVal := range t {
v, err := FromUnstructured(rawVal)
if err != nil {
return Value{}, fmt.Errorf("key %v: %v", k, err)
}
m.Set(k, v)
}
return Value{MapValue: &m}, nil
case yaml.MapSlice:
m := Map{}
for _, item := range t {
k, ok := item.Key.(string)
if !ok {
return Value{}, fmt.Errorf("key %#v is not a string", item.Key)
}
v, err := FromUnstructured(item.Value)
if err != nil {
return Value{}, fmt.Errorf("key %v: %v", k, err)
}
m.Set(k, v)
}
return Value{MapValue: &m}, nil
case []interface{}:
l := List{}
for i, rawVal := range t {
v, err := FromUnstructured(rawVal)
if err != nil {
return Value{}, fmt.Errorf("index %v: %v", i, err)
}
l.Items = append(l.Items, v)
}
return Value{ListValue: &l}, nil
case int:
n := Int(t)
return Value{IntValue: &n}, nil
case int8:
n := Int(t)
return Value{IntValue: &n}, nil
case int16:
n := Int(t)
return Value{IntValue: &n}, nil
case int32:
n := Int(t)
return Value{IntValue: &n}, nil
case int64:
n := Int(t)
return Value{IntValue: &n}, nil
case uint:
n := Int(t)
return Value{IntValue: &n}, nil
case uint8:
n := Int(t)
return Value{IntValue: &n}, nil
case uint16:
n := Int(t)
return Value{IntValue: &n}, nil
case uint32:
n := Int(t)
return Value{IntValue: &n}, nil
case float32:
f := Float(t)
return Value{FloatValue: &f}, nil
case float64:
f := Float(t)
return Value{FloatValue: &f}, nil
case string:
return StringValue(t), nil
case bool:
return BooleanValue(t), nil
default:
return Value{}, fmt.Errorf("type unimplemented: %t", in)
}
}
// ToYAML is a helper function for producing a YAML document; it attempts to
// preserve order of keys within maps/structs. This is as a convenience to
// humans keeping YAML documents, not because there is a behavior difference.
func (v *Value) ToYAML() ([]byte, error) {
return yaml.Marshal(v.ToUnstructured(true))
}
// ToJSON is a helper function for producing a JSon document.
func (v *Value) ToJSON() ([]byte, error) {
return json.Marshal(v.ToUnstructured(false))
}
// ToUnstructured will convert the Value into a go-typed object.
// If preserveOrder is true, then maps will be converted to the yaml.MapSlice
// type. Otherwise, map[string]interface{} must be used-- this destroys
// ordering information and is not recommended if the result of this will be
// serialized. Other types:
// * list -> []interface{}
// * others -> corresponding go type, wrapped in an interface{}
//
// Of note, floats and ints will always come out as float64 and int64,
// respectively.
func (v *Value) ToUnstructured(preserveOrder bool) interface{} {
switch {
case v.FloatValue != nil:
f := float64(*v.FloatValue)
return f
case v.IntValue != nil:
i := int64(*v.IntValue)
return i
case v.StringValue != nil:
return string(*v.StringValue)
case v.BooleanValue != nil:
return bool(*v.BooleanValue)
case v.ListValue != nil:
out := []interface{}{}
for _, item := range v.ListValue.Items {
out = append(out, item.ToUnstructured(preserveOrder))
}
return out
case v.MapValue != nil:
m := v.MapValue
if preserveOrder {
ms := make(yaml.MapSlice, len(m.Items))
for i := range m.Items {
ms[i] = yaml.MapItem{
Key: m.Items[i].Name,
Value: m.Items[i].Value.ToUnstructured(preserveOrder),
}
}
return ms
}
// This case is unavoidably lossy.
out := map[string]interface{}{}
for i := range m.Items {
out[m.Items[i].Name] = m.Items[i].Value.ToUnstructured(preserveOrder)
}
return out
default:
fallthrough
case v.Null == true:
return nil
}
}
/*
Copyright 2018 The Kubernetes 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 value
import (
"fmt"
"strings"
)
// A Value is an object; it corresponds to an 'atom' in the schema.
type Value struct {
// Exactly one of the below must be set.
FloatValue *Float
IntValue *Int
StringValue *String
BooleanValue *Boolean
ListValue *List
MapValue *Map
Null bool // represents an explicit `"foo" = null`
}
type Int int64
type Float float64
type String string
type Boolean bool
// Field is an individual key-value pair.
type Field struct {
Name string
Value Value
}
// List is a list of items.
type List struct {
Items []Value
}
// Map is a map of key-value pairs. It represents both structs and maps. We use
// a list and a go-language map to preserve order.
//
// Set and Get helpers are provided.
type Map struct {
Items []Field
// may be nil; lazily constructed.
// TODO: Direct modifications to Items above will cause serious problems.
index map[string]*Field
}
// Get returns the (Field, true) or (nil, false) if it is not present
func (m *Map) Get(key string) (*Field, bool) {
if m.index == nil {
m.index = map[string]*Field{}
for i := range m.Items {
f := &m.Items[i]
m.index[f.Name] = f
}
}
f, ok := m.index[key]
return f, ok
}
// Set inserts or updates the given item.
func (m *Map) Set(key string, value Value) {
if f, ok := m.Get(key); ok {
f.Value = value
return
}
m.Items = append(m.Items, Field{Name: key, Value: value})
m.index = nil // Since the append might have reallocated
}
// StringValue returns s as a scalar string Value.
func StringValue(s string) Value {
s2 := String(s)
return Value{StringValue: &s2}
}
// IntValue returns i as a scalar numeric (integer) Value.
func IntValue(i int) Value {
i2 := Int(i)
return Value{IntValue: &i2}
}
// FloatValue returns f as a scalar numeric (float) Value.
func FloatValue(f float64) Value {
f2 := Float(f)
return Value{FloatValue: &f2}
}
// BooleanValue returns b as a scalar boolean Value.
func BooleanValue(b bool) Value {
b2 := Boolean(b)
return Value{BooleanValue: &b2}
}
// String returns a human-readable representation of the value.
func (v Value) String() string {
switch {
case v.FloatValue != nil:
return fmt.Sprintf("%v", *v.FloatValue)
case v.IntValue != nil:
return fmt.Sprintf("%v", *v.IntValue)
case v.StringValue != nil:
return fmt.Sprintf("%q", *v.StringValue)
case v.BooleanValue != nil:
return fmt.Sprintf("%v", *v.BooleanValue)
case v.ListValue != nil:
strs := []string{}
for _, item := range v.ListValue.Items {
strs = append(strs, item.String())
}
return "[" + strings.Join(strs, ",") + "]"
case v.MapValue != nil:
strs := []string{}
for _, i := range v.MapValue.Items {
strs = append(strs, fmt.Sprintf("%v=%v", i.Name, i.Value))
}
return "{" + strings.Join(strs, ";") + "}"
default:
fallthrough
case v.Null == true:
return "null"
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment