app: added helm tgz handle

This commit is contained in:
2026-03-12 16:59:29 +02:00
parent 0d67944966
commit 95ed9ddb97
3182 changed files with 957097 additions and 133 deletions
+42
View File
@@ -0,0 +1,42 @@
# Contributing Guidelines
Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://git.k8s.io/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt:
_As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._
## Criteria for adding code here
This library adapts the stdlib `encoding/json` decoder to be compatible with
Kubernetes JSON decoding, and is not expected to actively add new features.
It may be updated with changes from the stdlib `encoding/json` decoder.
Any code that is added must:
* Have full unit test and benchmark coverage
* Be backward compatible with the existing exposed go API
* Have zero external dependencies
* Preserve existing benchmark performance
* Preserve compatibility with existing decoding behavior of `UnmarshalCaseSensitivePreserveInts()` or `UnmarshalStrict()`
* Avoid use of `unsafe`
## Getting Started
We have full documentation on how to get started contributing here:
<!---
If your repo has certain guidelines for contribution, put them here ahead of the general k8s resources
-->
- [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests
- [Kubernetes Contributor Guide](https://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](https://git.k8s.io/community/contributors/guide#contributing)
- [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet) - Common resources for existing developers
## Community, discussion, contribution, and support
You can reach the maintainers of this project via the
[sig-api-machinery mailing list / channels](https://github.com/kubernetes/community/tree/master/sig-api-machinery#contact).
## Mentorship
- [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers!
+238
View File
@@ -0,0 +1,238 @@
Files other than internal/golang/* licensed under:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.
------------------
internal/golang/* files licensed under:
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+6
View File
@@ -0,0 +1,6 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- deads2k
- jpbetz
- liggitt
+40
View File
@@ -0,0 +1,40 @@
# sigs.k8s.io/json
[![Go Reference](https://pkg.go.dev/badge/sigs.k8s.io/json.svg)](https://pkg.go.dev/sigs.k8s.io/json)
## Introduction
This library is a subproject of [sig-api-machinery](https://github.com/kubernetes/community/tree/master/sig-api-machinery#json).
It provides case-sensitive, integer-preserving JSON unmarshaling functions based on `encoding/json` `Unmarshal()`.
## Compatibility
The `UnmarshalCaseSensitivePreserveInts()` function behaves like `encoding/json#Unmarshal()` with the following differences:
- JSON object keys are treated case-sensitively.
Object keys must exactly match json tag names (for tagged struct fields)
or struct field names (for untagged struct fields).
- JSON integers are unmarshaled into `interface{}` fields as an `int64` instead of a
`float64` when possible, falling back to `float64` on any parse or overflow error.
- Syntax errors do not return an `encoding/json` `*SyntaxError` error.
Instead, they return an error which can be passed to `SyntaxErrorOffset()` to obtain an offset.
## Additional capabilities
The `UnmarshalStrict()` function decodes identically to `UnmarshalCaseSensitivePreserveInts()`,
and also returns non-fatal strict errors encountered while decoding:
- Duplicate fields encountered
- Unknown fields encountered
### Community, discussion, contribution, and support
You can reach the maintainers of this project via the
[sig-api-machinery mailing list / channels](https://github.com/kubernetes/community/tree/master/sig-api-machinery#contact).
### Code of conduct
Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md).
[owners]: https://git.k8s.io/community/contributors/guide/owners.md
[Creative Commons 4.0]: https://git.k8s.io/website/LICENSE
+22
View File
@@ -0,0 +1,22 @@
# Security Policy
## Security Announcements
Join the [kubernetes-security-announce] group for security and vulnerability announcements.
You can also subscribe to an RSS feed of the above using [this link][kubernetes-security-announce-rss].
## Reporting a Vulnerability
Instructions for reporting a vulnerability can be found on the
[Kubernetes Security and Disclosure Information] page.
## Supported Versions
Information about supported Kubernetes versions can be found on the
[Kubernetes version and version skew support policy] page on the Kubernetes website.
[kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce
[kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50
[Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions
[Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability
+15
View File
@@ -0,0 +1,15 @@
# Defined below are the security contacts for this repo.
#
# They are the contact point for the Product Security Committee to reach out
# to for triaging and handling of incoming issues.
#
# The below names agree to abide by the
# [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy)
# and will be removed and replaced if they violate that agreement.
#
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
# INSTRUCTIONS AT https://kubernetes.io/security/
deads2k
lavalamp
liggitt
+3
View File
@@ -0,0 +1,3 @@
# Kubernetes Community Code of Conduct
Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md)
+17
View File
@@ -0,0 +1,17 @@
/*
Copyright 2021 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 json // import "sigs.k8s.io/json"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
// 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 json
import (
"unicode"
"unicode/utf8"
)
// foldName returns a folded string such that foldName(x) == foldName(y)
// is identical to bytes.EqualFold(x, y).
func foldName(in []byte) []byte {
// This is inlinable to take advantage of "function outlining".
var arr [32]byte // large enough for most JSON names
return appendFoldedName(arr[:0], in)
}
func appendFoldedName(out, in []byte) []byte {
for i := 0; i < len(in); {
// Handle single-byte ASCII.
if c := in[i]; c < utf8.RuneSelf {
if 'a' <= c && c <= 'z' {
c -= 'a' - 'A'
}
out = append(out, c)
i++
continue
}
// Handle multi-byte Unicode.
r, n := utf8.DecodeRune(in[i:])
out = utf8.AppendRune(out, foldRune(r))
i += n
}
return out
}
// foldRune is returns the smallest rune for all runes in the same fold set.
func foldRune(r rune) rune {
for {
r2 := unicode.SimpleFold(r)
if r2 <= r {
return r2
}
r = r2
}
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2019 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.
//go:build gofuzz
package json
import (
"fmt"
)
func Fuzz(data []byte) (score int) {
for _, ctor := range []func() any{
func() any { return new(any) },
func() any { return new(map[string]any) },
func() any { return new([]any) },
} {
v := ctor()
err := Unmarshal(data, v)
if err != nil {
continue
}
score = 1
m, err := Marshal(v)
if err != nil {
fmt.Printf("v=%#v\n", v)
panic(err)
}
u := ctor()
err = Unmarshal(m, u)
if err != nil {
fmt.Printf("v=%#v\n", v)
fmt.Printf("m=%s\n", m)
panic(err)
}
}
return
}
+182
View File
@@ -0,0 +1,182 @@
// Copyright 2010 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 json
import "bytes"
// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029
// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029
// so that the JSON will be safe to embed inside HTML <script> tags.
// For historical reasons, web browsers don't honor standard HTML
// escaping within <script> tags, so an alternative JSON encoding must be used.
func HTMLEscape(dst *bytes.Buffer, src []byte) {
dst.Grow(len(src))
dst.Write(appendHTMLEscape(dst.AvailableBuffer(), src))
}
func appendHTMLEscape(dst, src []byte) []byte {
// The characters can only appear in string literals,
// so just scan the string one byte at a time.
start := 0
for i, c := range src {
if c == '<' || c == '>' || c == '&' {
dst = append(dst, src[start:i]...)
dst = append(dst, '\\', 'u', '0', '0', hex[c>>4], hex[c&0xF])
start = i + 1
}
// Convert U+2028 and U+2029 (E2 80 A8 and E2 80 A9).
if c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 {
dst = append(dst, src[start:i]...)
dst = append(dst, '\\', 'u', '2', '0', '2', hex[src[i+2]&0xF])
start = i + len("\u2029")
}
}
return append(dst, src[start:]...)
}
// Compact appends to dst the JSON-encoded src with
// insignificant space characters elided.
func Compact(dst *bytes.Buffer, src []byte) error {
dst.Grow(len(src))
b := dst.AvailableBuffer()
b, err := appendCompact(b, src, false)
dst.Write(b)
return err
}
func appendCompact(dst, src []byte, escape bool) ([]byte, error) {
origLen := len(dst)
scan := newScanner()
defer freeScanner(scan)
start := 0
for i, c := range src {
if escape && (c == '<' || c == '>' || c == '&') {
if start < i {
dst = append(dst, src[start:i]...)
}
dst = append(dst, '\\', 'u', '0', '0', hex[c>>4], hex[c&0xF])
start = i + 1
}
// Convert U+2028 and U+2029 (E2 80 A8 and E2 80 A9).
if escape && c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 {
if start < i {
dst = append(dst, src[start:i]...)
}
dst = append(dst, '\\', 'u', '2', '0', '2', hex[src[i+2]&0xF])
start = i + 3
}
v := scan.step(scan, c)
if v >= scanSkipSpace {
if v == scanError {
break
}
if start < i {
dst = append(dst, src[start:i]...)
}
start = i + 1
}
}
if scan.eof() == scanError {
return dst[:origLen], scan.err
}
if start < len(src) {
dst = append(dst, src[start:]...)
}
return dst, nil
}
func appendNewline(dst []byte, prefix, indent string, depth int) []byte {
dst = append(dst, '\n')
dst = append(dst, prefix...)
for i := 0; i < depth; i++ {
dst = append(dst, indent...)
}
return dst
}
// indentGrowthFactor specifies the growth factor of indenting JSON input.
// Empirically, the growth factor was measured to be between 1.4x to 1.8x
// for some set of compacted JSON with the indent being a single tab.
// Specify a growth factor slightly larger than what is observed
// to reduce probability of allocation in appendIndent.
// A factor no higher than 2 ensures that wasted space never exceeds 50%.
const indentGrowthFactor = 2
// Indent appends to dst an indented form of the JSON-encoded src.
// Each element in a JSON object or array begins on a new,
// indented line beginning with prefix followed by one or more
// copies of indent according to the indentation nesting.
// The data appended to dst does not begin with the prefix nor
// any indentation, to make it easier to embed inside other formatted JSON data.
// Although leading space characters (space, tab, carriage return, newline)
// at the beginning of src are dropped, trailing space characters
// at the end of src are preserved and copied to dst.
// For example, if src has no trailing spaces, neither will dst;
// if src ends in a trailing newline, so will dst.
func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
dst.Grow(indentGrowthFactor * len(src))
b := dst.AvailableBuffer()
b, err := appendIndent(b, src, prefix, indent)
dst.Write(b)
return err
}
func appendIndent(dst, src []byte, prefix, indent string) ([]byte, error) {
origLen := len(dst)
scan := newScanner()
defer freeScanner(scan)
needIndent := false
depth := 0
for _, c := range src {
scan.bytes++
v := scan.step(scan, c)
if v == scanSkipSpace {
continue
}
if v == scanError {
break
}
if needIndent && v != scanEndObject && v != scanEndArray {
needIndent = false
depth++
dst = appendNewline(dst, prefix, indent, depth)
}
// Emit semantically uninteresting bytes
// (in particular, punctuation in strings) unmodified.
if v == scanContinue {
dst = append(dst, c)
continue
}
// Add spacing around real punctuation.
switch c {
case '{', '[':
// delay indent so that empty object and array are formatted as {} and [].
needIndent = true
dst = append(dst, c)
case ',':
dst = append(dst, c)
dst = appendNewline(dst, prefix, indent, depth)
case ':':
dst = append(dst, c, ' ')
case '}', ']':
if needIndent {
// suppress indent in empty object/array
needIndent = false
} else {
depth--
dst = appendNewline(dst, prefix, indent, depth)
}
dst = append(dst, c)
default:
dst = append(dst, c)
}
}
if scan.eof() == scanError {
return dst[:origLen], scan.err
}
return dst, nil
}
@@ -0,0 +1,168 @@
/*
Copyright 2021 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 json
import (
gojson "encoding/json"
"strconv"
"strings"
)
// Type-alias error and data types returned from decoding
type UnmarshalTypeError = gojson.UnmarshalTypeError
type UnmarshalFieldError = gojson.UnmarshalFieldError
type InvalidUnmarshalError = gojson.InvalidUnmarshalError
type Number = gojson.Number
type RawMessage = gojson.RawMessage
type Token = gojson.Token
type Delim = gojson.Delim
type UnmarshalOpt func(*decodeState)
func UseNumber(d *decodeState) {
d.useNumber = true
}
func DisallowUnknownFields(d *decodeState) {
d.disallowUnknownFields = true
}
// CaseSensitive requires json keys to exactly match specified json tags (for tagged struct fields)
// or struct field names (for untagged struct fields), or be treated as an unknown field.
func CaseSensitive(d *decodeState) {
d.caseSensitive = true
}
func (d *Decoder) CaseSensitive() {
d.d.caseSensitive = true
}
// PreserveInts decodes numbers as int64 when decoding to untyped fields,
// if the JSON data does not contain a "." character, parses as an integer successfully,
// and does not overflow int64. Otherwise, it falls back to default float64 decoding behavior.
//
// If UseNumber is also set, it takes precedence over PreserveInts.
func PreserveInts(d *decodeState) {
d.preserveInts = true
}
func (d *Decoder) PreserveInts() {
d.d.preserveInts = true
}
// DisallowDuplicateFields treats duplicate fields encountered while decoding as an error.
func DisallowDuplicateFields(d *decodeState) {
d.disallowDuplicateFields = true
}
func (d *Decoder) DisallowDuplicateFields() {
d.d.disallowDuplicateFields = true
}
func (d *decodeState) newFieldError(errType strictErrType, field string) *strictError {
if len(d.strictFieldStack) > 0 {
return &strictError{
ErrType: errType,
Path: strings.Join(d.strictFieldStack, "") + "." + field,
}
} else {
return &strictError{
ErrType: errType,
Path: field,
}
}
}
// saveStrictError saves a strict decoding error,
// for reporting at the end of the unmarshal if no other errors occurred.
func (d *decodeState) saveStrictError(err *strictError) {
// prevent excessive numbers of accumulated errors
if len(d.savedStrictErrors) >= 100 {
return
}
// dedupe accumulated strict errors
if d.seenStrictErrors == nil {
d.seenStrictErrors = map[strictError]struct{}{}
}
if _, seen := d.seenStrictErrors[*err]; seen {
return
}
// accumulate the error
d.seenStrictErrors[*err] = struct{}{}
d.savedStrictErrors = append(d.savedStrictErrors, err)
}
func (d *decodeState) appendStrictFieldStackKey(key string) {
if !d.disallowDuplicateFields && !d.disallowUnknownFields {
return
}
if len(d.strictFieldStack) > 0 {
d.strictFieldStack = append(d.strictFieldStack, ".", key)
} else {
d.strictFieldStack = append(d.strictFieldStack, key)
}
}
func (d *decodeState) appendStrictFieldStackIndex(i int) {
if !d.disallowDuplicateFields && !d.disallowUnknownFields {
return
}
d.strictFieldStack = append(d.strictFieldStack, "[", strconv.Itoa(i), "]")
}
type strictErrType string
const (
unknownStrictErrType strictErrType = "unknown field"
duplicateStrictErrType strictErrType = "duplicate field"
)
// strictError is a strict decoding error
// It has an ErrType (either unknown or duplicate)
// and a path to the erroneous field
type strictError struct {
ErrType strictErrType
Path string
}
func (e *strictError) Error() string {
return string(e.ErrType) + " " + strconv.Quote(e.Path)
}
func (e *strictError) FieldPath() string {
return e.Path
}
func (e *strictError) SetFieldPath(path string) {
e.Path = path
}
// UnmarshalStrictError holds errors resulting from use of strict disallow___ decoder directives.
// If this is returned from Unmarshal(), it means the decoding was successful in all other respects.
type UnmarshalStrictError struct {
Errors []error
}
func (e *UnmarshalStrictError) Error() string {
var b strings.Builder
b.WriteString("json: ")
for i, err := range e.Errors {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(err.Error())
}
return b.String()
}
+610
View File
@@ -0,0 +1,610 @@
// Copyright 2010 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 json
// JSON value parser state machine.
// Just about at the limit of what is reasonable to write by hand.
// Some parts are a bit tedious, but overall it nicely factors out the
// otherwise common code from the multiple scanning functions
// in this package (Compact, Indent, checkValid, etc).
//
// This file starts with two simple examples using the scanner
// before diving into the scanner itself.
import (
"strconv"
"sync"
)
// Valid reports whether data is a valid JSON encoding.
func Valid(data []byte) bool {
scan := newScanner()
defer freeScanner(scan)
return checkValid(data, scan) == nil
}
// checkValid verifies that data is valid JSON-encoded data.
// scan is passed in for use by checkValid to avoid an allocation.
// checkValid returns nil or a SyntaxError.
func checkValid(data []byte, scan *scanner) error {
scan.reset()
for _, c := range data {
scan.bytes++
if scan.step(scan, c) == scanError {
return scan.err
}
}
if scan.eof() == scanError {
return scan.err
}
return nil
}
// A SyntaxError is a description of a JSON syntax error.
// [Unmarshal] will return a SyntaxError if the JSON can't be parsed.
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
// A scanner is a JSON scanning state machine.
// Callers call scan.reset and then pass bytes in one at a time
// by calling scan.step(&scan, c) for each byte.
// The return value, referred to as an opcode, tells the
// caller about significant parsing events like beginning
// and ending literals, objects, and arrays, so that the
// caller can follow along if it wishes.
// The return value scanEnd indicates that a single top-level
// JSON value has been completed, *before* the byte that
// just got passed in. (The indication must be delayed in order
// to recognize the end of numbers: is 123 a whole value or
// the beginning of 12345e+6?).
type scanner struct {
// The step is a func to be called to execute the next transition.
// Also tried using an integer constant and a single func
// with a switch, but using the func directly was 10% faster
// on a 64-bit Mac Mini, and it's nicer to read.
step func(*scanner, byte) int
// Reached end of top-level value.
endTop bool
// Stack of what we're in the middle of - array values, object keys, object values.
parseState []int
// Error that happened, if any.
err error
// total bytes consumed, updated by decoder.Decode (and deliberately
// not set to zero by scan.reset)
bytes int64
}
var scannerPool = sync.Pool{
New: func() any {
return &scanner{}
},
}
func newScanner() *scanner {
scan := scannerPool.Get().(*scanner)
// scan.reset by design doesn't set bytes to zero
scan.bytes = 0
scan.reset()
return scan
}
func freeScanner(scan *scanner) {
// Avoid hanging on to too much memory in extreme cases.
if len(scan.parseState) > 1024 {
scan.parseState = nil
}
scannerPool.Put(scan)
}
// These values are returned by the state transition functions
// assigned to scanner.state and the method scanner.eof.
// They give details about the current state of the scan that
// callers might be interested to know about.
// It is okay to ignore the return value of any particular
// call to scanner.state: if one call returns scanError,
// every subsequent call will return scanError too.
const (
// Continue.
scanContinue = iota // uninteresting byte
scanBeginLiteral // end implied by next result != scanContinue
scanBeginObject // begin object
scanObjectKey // just finished object key (string)
scanObjectValue // just finished non-last object value
scanEndObject // end object (implies scanObjectValue if possible)
scanBeginArray // begin array
scanArrayValue // just finished array value
scanEndArray // end array (implies scanArrayValue if possible)
scanSkipSpace // space byte; can skip; known to be last "continue" result
// Stop.
scanEnd // top-level value ended *before* this byte; known to be first "stop" result
scanError // hit an error, scanner.err.
)
// These values are stored in the parseState stack.
// They give the current state of a composite value
// being scanned. If the parser is inside a nested value
// the parseState describes the nested state, outermost at entry 0.
const (
parseObjectKey = iota // parsing object key (before colon)
parseObjectValue // parsing object value (after colon)
parseArrayValue // parsing array value
)
// This limits the max nesting depth to prevent stack overflow.
// This is permitted by https://tools.ietf.org/html/rfc7159#section-9
const maxNestingDepth = 10000
// reset prepares the scanner for use.
// It must be called before calling s.step.
func (s *scanner) reset() {
s.step = stateBeginValue
s.parseState = s.parseState[0:0]
s.err = nil
s.endTop = false
}
// eof tells the scanner that the end of input has been reached.
// It returns a scan status just as s.step does.
func (s *scanner) eof() int {
if s.err != nil {
return scanError
}
if s.endTop {
return scanEnd
}
s.step(s, ' ')
if s.endTop {
return scanEnd
}
if s.err == nil {
s.err = &SyntaxError{"unexpected end of JSON input", s.bytes}
}
return scanError
}
// pushParseState pushes a new parse state p onto the parse stack.
// an error state is returned if maxNestingDepth was exceeded, otherwise successState is returned.
func (s *scanner) pushParseState(c byte, newParseState int, successState int) int {
s.parseState = append(s.parseState, newParseState)
if len(s.parseState) <= maxNestingDepth {
return successState
}
return s.error(c, "exceeded max depth")
}
// popParseState pops a parse state (already obtained) off the stack
// and updates s.step accordingly.
func (s *scanner) popParseState() {
n := len(s.parseState) - 1
s.parseState = s.parseState[0:n]
if n == 0 {
s.step = stateEndTop
s.endTop = true
} else {
s.step = stateEndValue
}
}
func isSpace(c byte) bool {
return c <= ' ' && (c == ' ' || c == '\t' || c == '\r' || c == '\n')
}
// stateBeginValueOrEmpty is the state after reading `[`.
func stateBeginValueOrEmpty(s *scanner, c byte) int {
if isSpace(c) {
return scanSkipSpace
}
if c == ']' {
return stateEndValue(s, c)
}
return stateBeginValue(s, c)
}
// stateBeginValue is the state at the beginning of the input.
func stateBeginValue(s *scanner, c byte) int {
if isSpace(c) {
return scanSkipSpace
}
switch c {
case '{':
s.step = stateBeginStringOrEmpty
return s.pushParseState(c, parseObjectKey, scanBeginObject)
case '[':
s.step = stateBeginValueOrEmpty
return s.pushParseState(c, parseArrayValue, scanBeginArray)
case '"':
s.step = stateInString
return scanBeginLiteral
case '-':
s.step = stateNeg
return scanBeginLiteral
case '0': // beginning of 0.123
s.step = state0
return scanBeginLiteral
case 't': // beginning of true
s.step = stateT
return scanBeginLiteral
case 'f': // beginning of false
s.step = stateF
return scanBeginLiteral
case 'n': // beginning of null
s.step = stateN
return scanBeginLiteral
}
if '1' <= c && c <= '9' { // beginning of 1234.5
s.step = state1
return scanBeginLiteral
}
return s.error(c, "looking for beginning of value")
}
// stateBeginStringOrEmpty is the state after reading `{`.
func stateBeginStringOrEmpty(s *scanner, c byte) int {
if isSpace(c) {
return scanSkipSpace
}
if c == '}' {
n := len(s.parseState)
s.parseState[n-1] = parseObjectValue
return stateEndValue(s, c)
}
return stateBeginString(s, c)
}
// stateBeginString is the state after reading `{"key": value,`.
func stateBeginString(s *scanner, c byte) int {
if isSpace(c) {
return scanSkipSpace
}
if c == '"' {
s.step = stateInString
return scanBeginLiteral
}
return s.error(c, "looking for beginning of object key string")
}
// stateEndValue is the state after completing a value,
// such as after reading `{}` or `true` or `["x"`.
func stateEndValue(s *scanner, c byte) int {
n := len(s.parseState)
if n == 0 {
// Completed top-level before the current byte.
s.step = stateEndTop
s.endTop = true
return stateEndTop(s, c)
}
if isSpace(c) {
s.step = stateEndValue
return scanSkipSpace
}
ps := s.parseState[n-1]
switch ps {
case parseObjectKey:
if c == ':' {
s.parseState[n-1] = parseObjectValue
s.step = stateBeginValue
return scanObjectKey
}
return s.error(c, "after object key")
case parseObjectValue:
if c == ',' {
s.parseState[n-1] = parseObjectKey
s.step = stateBeginString
return scanObjectValue
}
if c == '}' {
s.popParseState()
return scanEndObject
}
return s.error(c, "after object key:value pair")
case parseArrayValue:
if c == ',' {
s.step = stateBeginValue
return scanArrayValue
}
if c == ']' {
s.popParseState()
return scanEndArray
}
return s.error(c, "after array element")
}
return s.error(c, "")
}
// stateEndTop is the state after finishing the top-level value,
// such as after reading `{}` or `[1,2,3]`.
// Only space characters should be seen now.
func stateEndTop(s *scanner, c byte) int {
if !isSpace(c) {
// Complain about non-space byte on next call.
s.error(c, "after top-level value")
}
return scanEnd
}
// stateInString is the state after reading `"`.
func stateInString(s *scanner, c byte) int {
if c == '"' {
s.step = stateEndValue
return scanContinue
}
if c == '\\' {
s.step = stateInStringEsc
return scanContinue
}
if c < 0x20 {
return s.error(c, "in string literal")
}
return scanContinue
}
// stateInStringEsc is the state after reading `"\` during a quoted string.
func stateInStringEsc(s *scanner, c byte) int {
switch c {
case 'b', 'f', 'n', 'r', 't', '\\', '/', '"':
s.step = stateInString
return scanContinue
case 'u':
s.step = stateInStringEscU
return scanContinue
}
return s.error(c, "in string escape code")
}
// stateInStringEscU is the state after reading `"\u` during a quoted string.
func stateInStringEscU(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU1
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU1 is the state after reading `"\u1` during a quoted string.
func stateInStringEscU1(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU12
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU12 is the state after reading `"\u12` during a quoted string.
func stateInStringEscU12(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU123
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU123 is the state after reading `"\u123` during a quoted string.
func stateInStringEscU123(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInString
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateNeg is the state after reading `-` during a number.
func stateNeg(s *scanner, c byte) int {
if c == '0' {
s.step = state0
return scanContinue
}
if '1' <= c && c <= '9' {
s.step = state1
return scanContinue
}
return s.error(c, "in numeric literal")
}
// state1 is the state after reading a non-zero integer during a number,
// such as after reading `1` or `100` but not `0`.
func state1(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = state1
return scanContinue
}
return state0(s, c)
}
// state0 is the state after reading `0` during a number.
func state0(s *scanner, c byte) int {
if c == '.' {
s.step = stateDot
return scanContinue
}
if c == 'e' || c == 'E' {
s.step = stateE
return scanContinue
}
return stateEndValue(s, c)
}
// stateDot is the state after reading the integer and decimal point in a number,
// such as after reading `1.`.
func stateDot(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = stateDot0
return scanContinue
}
return s.error(c, "after decimal point in numeric literal")
}
// stateDot0 is the state after reading the integer, decimal point, and subsequent
// digits of a number, such as after reading `3.14`.
func stateDot0(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
return scanContinue
}
if c == 'e' || c == 'E' {
s.step = stateE
return scanContinue
}
return stateEndValue(s, c)
}
// stateE is the state after reading the mantissa and e in a number,
// such as after reading `314e` or `0.314e`.
func stateE(s *scanner, c byte) int {
if c == '+' || c == '-' {
s.step = stateESign
return scanContinue
}
return stateESign(s, c)
}
// stateESign is the state after reading the mantissa, e, and sign in a number,
// such as after reading `314e-` or `0.314e+`.
func stateESign(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = stateE0
return scanContinue
}
return s.error(c, "in exponent of numeric literal")
}
// stateE0 is the state after reading the mantissa, e, optional sign,
// and at least one digit of the exponent in a number,
// such as after reading `314e-2` or `0.314e+1` or `3.14e0`.
func stateE0(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
return scanContinue
}
return stateEndValue(s, c)
}
// stateT is the state after reading `t`.
func stateT(s *scanner, c byte) int {
if c == 'r' {
s.step = stateTr
return scanContinue
}
return s.error(c, "in literal true (expecting 'r')")
}
// stateTr is the state after reading `tr`.
func stateTr(s *scanner, c byte) int {
if c == 'u' {
s.step = stateTru
return scanContinue
}
return s.error(c, "in literal true (expecting 'u')")
}
// stateTru is the state after reading `tru`.
func stateTru(s *scanner, c byte) int {
if c == 'e' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal true (expecting 'e')")
}
// stateF is the state after reading `f`.
func stateF(s *scanner, c byte) int {
if c == 'a' {
s.step = stateFa
return scanContinue
}
return s.error(c, "in literal false (expecting 'a')")
}
// stateFa is the state after reading `fa`.
func stateFa(s *scanner, c byte) int {
if c == 'l' {
s.step = stateFal
return scanContinue
}
return s.error(c, "in literal false (expecting 'l')")
}
// stateFal is the state after reading `fal`.
func stateFal(s *scanner, c byte) int {
if c == 's' {
s.step = stateFals
return scanContinue
}
return s.error(c, "in literal false (expecting 's')")
}
// stateFals is the state after reading `fals`.
func stateFals(s *scanner, c byte) int {
if c == 'e' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal false (expecting 'e')")
}
// stateN is the state after reading `n`.
func stateN(s *scanner, c byte) int {
if c == 'u' {
s.step = stateNu
return scanContinue
}
return s.error(c, "in literal null (expecting 'u')")
}
// stateNu is the state after reading `nu`.
func stateNu(s *scanner, c byte) int {
if c == 'l' {
s.step = stateNul
return scanContinue
}
return s.error(c, "in literal null (expecting 'l')")
}
// stateNul is the state after reading `nul`.
func stateNul(s *scanner, c byte) int {
if c == 'l' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal null (expecting 'l')")
}
// stateError is the state after reaching a syntax error,
// such as after reading `[1}` or `5.1.2`.
func stateError(s *scanner, c byte) int {
return scanError
}
// error records an error and switches to the error state.
func (s *scanner) error(c byte, context string) int {
s.step = stateError
s.err = &SyntaxError{"invalid character " + quoteChar(c) + " " + context, s.bytes}
return scanError
}
// quoteChar formats c as a quoted character literal.
func quoteChar(c byte) string {
// special cases - different from quoted strings
if c == '\'' {
return `'\''`
}
if c == '"' {
return `'"'`
}
// use quoted string with different quotation marks
s := strconv.Quote(string(c))
return "'" + s[1:len(s)-1] + "'"
}
+517
View File
@@ -0,0 +1,517 @@
// Copyright 2010 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 json
import (
"bytes"
"io"
)
// A Decoder reads and decodes JSON values from an input stream.
type Decoder struct {
r io.Reader
buf []byte
d decodeState
scanp int // start of unread data in buf
scanned int64 // amount of data already scanned
scan scanner
err error
tokenState int
tokenStack []int
}
// NewDecoder returns a new decoder that reads from r.
//
// The decoder introduces its own buffering and may
// read data from r beyond the JSON values requested.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r: r}
}
// UseNumber causes the Decoder to unmarshal a number into an
// interface value as a [Number] instead of as a float64.
func (dec *Decoder) UseNumber() { dec.d.useNumber = true }
// DisallowUnknownFields causes the Decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
func (dec *Decoder) DisallowUnknownFields() { dec.d.disallowUnknownFields = true }
// Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v.
//
// See the documentation for [Unmarshal] for details about
// the conversion of JSON into a Go value.
func (dec *Decoder) Decode(v any) error {
if dec.err != nil {
return dec.err
}
if err := dec.tokenPrepareForDecode(); err != nil {
return err
}
if !dec.tokenValueAllowed() {
return &SyntaxError{msg: "not at beginning of value", Offset: dec.InputOffset()}
}
// Read whole value into buffer.
n, err := dec.readValue()
if err != nil {
return err
}
dec.d.init(dec.buf[dec.scanp : dec.scanp+n])
dec.scanp += n
// Don't save err from unmarshal into dec.err:
// the connection is still usable since we read a complete JSON
// object from it before the error happened.
err = dec.d.unmarshal(v)
// fixup token streaming state
dec.tokenValueEnd()
return err
}
// Buffered returns a reader of the data remaining in the Decoder's
// buffer. The reader is valid until the next call to [Decoder.Decode].
func (dec *Decoder) Buffered() io.Reader {
return bytes.NewReader(dec.buf[dec.scanp:])
}
// readValue reads a JSON value into dec.buf.
// It returns the length of the encoding.
func (dec *Decoder) readValue() (int, error) {
dec.scan.reset()
scanp := dec.scanp
var err error
Input:
// help the compiler see that scanp is never negative, so it can remove
// some bounds checks below.
for scanp >= 0 {
// Look in the buffer for a new value.
for ; scanp < len(dec.buf); scanp++ {
c := dec.buf[scanp]
dec.scan.bytes++
switch dec.scan.step(&dec.scan, c) {
case scanEnd:
// scanEnd is delayed one byte so we decrement
// the scanner bytes count by 1 to ensure that
// this value is correct in the next call of Decode.
dec.scan.bytes--
break Input
case scanEndObject, scanEndArray:
// scanEnd is delayed one byte.
// We might block trying to get that byte from src,
// so instead invent a space byte.
if stateEndValue(&dec.scan, ' ') == scanEnd {
scanp++
break Input
}
case scanError:
dec.err = dec.scan.err
return 0, dec.scan.err
}
}
// Did the last read have an error?
// Delayed until now to allow buffer scan.
if err != nil {
if err == io.EOF {
if dec.scan.step(&dec.scan, ' ') == scanEnd {
break Input
}
if nonSpace(dec.buf) {
err = io.ErrUnexpectedEOF
}
}
dec.err = err
return 0, err
}
n := scanp - dec.scanp
err = dec.refill()
scanp = dec.scanp + n
}
return scanp - dec.scanp, nil
}
func (dec *Decoder) refill() error {
// Make room to read more into the buffer.
// First slide down data already consumed.
if dec.scanp > 0 {
dec.scanned += int64(dec.scanp)
n := copy(dec.buf, dec.buf[dec.scanp:])
dec.buf = dec.buf[:n]
dec.scanp = 0
}
// Grow buffer if not large enough.
const minRead = 512
if cap(dec.buf)-len(dec.buf) < minRead {
newBuf := make([]byte, len(dec.buf), 2*cap(dec.buf)+minRead)
copy(newBuf, dec.buf)
dec.buf = newBuf
}
// Read. Delay error for next iteration (after scan).
n, err := dec.r.Read(dec.buf[len(dec.buf):cap(dec.buf)])
dec.buf = dec.buf[0 : len(dec.buf)+n]
return err
}
func nonSpace(b []byte) bool {
for _, c := range b {
if !isSpace(c) {
return true
}
}
return false
}
// An Encoder writes JSON values to an output stream.
type Encoder struct {
w io.Writer
err error
escapeHTML bool
indentBuf []byte
indentPrefix string
indentValue string
}
// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w: w, escapeHTML: true}
}
// Encode writes the JSON encoding of v to the stream,
// with insignificant space characters elided,
// followed by a newline character.
//
// See the documentation for [Marshal] for details about the
// conversion of Go values to JSON.
func (enc *Encoder) Encode(v any) error {
if enc.err != nil {
return enc.err
}
e := newEncodeState()
defer encodeStatePool.Put(e)
err := e.marshal(v, encOpts{escapeHTML: enc.escapeHTML})
if err != nil {
return err
}
// Terminate each value with a newline.
// This makes the output look a little nicer
// when debugging, and some kind of space
// is required if the encoded value was a number,
// so that the reader knows there aren't more
// digits coming.
e.WriteByte('\n')
b := e.Bytes()
if enc.indentPrefix != "" || enc.indentValue != "" {
enc.indentBuf, err = appendIndent(enc.indentBuf[:0], b, enc.indentPrefix, enc.indentValue)
if err != nil {
return err
}
b = enc.indentBuf
}
if _, err = enc.w.Write(b); err != nil {
enc.err = err
}
return err
}
// SetIndent instructs the encoder to format each subsequent encoded
// value as if indented by the package-level function Indent(dst, src, prefix, indent).
// Calling SetIndent("", "") disables indentation.
func (enc *Encoder) SetIndent(prefix, indent string) {
enc.indentPrefix = prefix
enc.indentValue = indent
}
// SetEscapeHTML specifies whether problematic HTML characters
// should be escaped inside JSON quoted strings.
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e
// to avoid certain safety problems that can arise when embedding JSON in HTML.
//
// In non-HTML settings where the escaping interferes with the readability
// of the output, SetEscapeHTML(false) disables this behavior.
func (enc *Encoder) SetEscapeHTML(on bool) {
enc.escapeHTML = on
}
/*
// RawMessage is a raw encoded JSON value.
// It implements [Marshaler] and [Unmarshaler] and can
// be used to delay JSON decoding or precompute a JSON encoding.
type RawMessage []byte
// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
if m == nil {
return []byte("null"), nil
}
return m, nil
}
// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
*m = append((*m)[0:0], data...)
return nil
}
*/
var _ Marshaler = (*RawMessage)(nil)
var _ Unmarshaler = (*RawMessage)(nil)
/*
// A Token holds a value of one of these types:
//
// - [Delim], for the four JSON delimiters [ ] { }
// - bool, for JSON booleans
// - float64, for JSON numbers
// - [Number], for JSON numbers
// - string, for JSON string literals
// - nil, for JSON null
type Token any
*/
const (
tokenTopValue = iota
tokenArrayStart
tokenArrayValue
tokenArrayComma
tokenObjectStart
tokenObjectKey
tokenObjectColon
tokenObjectValue
tokenObjectComma
)
// advance tokenstate from a separator state to a value state
func (dec *Decoder) tokenPrepareForDecode() error {
// Note: Not calling peek before switch, to avoid
// putting peek into the standard Decode path.
// peek is only called when using the Token API.
switch dec.tokenState {
case tokenArrayComma:
c, err := dec.peek()
if err != nil {
return err
}
if c != ',' {
return &SyntaxError{"expected comma after array element", dec.InputOffset()}
}
dec.scanp++
dec.tokenState = tokenArrayValue
case tokenObjectColon:
c, err := dec.peek()
if err != nil {
return err
}
if c != ':' {
return &SyntaxError{"expected colon after object key", dec.InputOffset()}
}
dec.scanp++
dec.tokenState = tokenObjectValue
}
return nil
}
func (dec *Decoder) tokenValueAllowed() bool {
switch dec.tokenState {
case tokenTopValue, tokenArrayStart, tokenArrayValue, tokenObjectValue:
return true
}
return false
}
func (dec *Decoder) tokenValueEnd() {
switch dec.tokenState {
case tokenArrayStart, tokenArrayValue:
dec.tokenState = tokenArrayComma
case tokenObjectValue:
dec.tokenState = tokenObjectComma
}
}
/*
// A Delim is a JSON array or object delimiter, one of [ ] { or }.
type Delim rune
func (d Delim) String() string {
return string(d)
}
*/
// Token returns the next JSON token in the input stream.
// At the end of the input stream, Token returns nil, [io.EOF].
//
// Token guarantees that the delimiters [ ] { } it returns are
// properly nested and matched: if Token encounters an unexpected
// delimiter in the input, it will return an error.
//
// The input stream consists of basic JSON values—bool, string,
// number, and null—along with delimiters [ ] { } of type [Delim]
// to mark the start and end of arrays and objects.
// Commas and colons are elided.
func (dec *Decoder) Token() (Token, error) {
for {
c, err := dec.peek()
if err != nil {
return nil, err
}
switch c {
case '[':
if !dec.tokenValueAllowed() {
return dec.tokenError(c)
}
dec.scanp++
dec.tokenStack = append(dec.tokenStack, dec.tokenState)
dec.tokenState = tokenArrayStart
return Delim('['), nil
case ']':
if dec.tokenState != tokenArrayStart && dec.tokenState != tokenArrayComma {
return dec.tokenError(c)
}
dec.scanp++
dec.tokenState = dec.tokenStack[len(dec.tokenStack)-1]
dec.tokenStack = dec.tokenStack[:len(dec.tokenStack)-1]
dec.tokenValueEnd()
return Delim(']'), nil
case '{':
if !dec.tokenValueAllowed() {
return dec.tokenError(c)
}
dec.scanp++
dec.tokenStack = append(dec.tokenStack, dec.tokenState)
dec.tokenState = tokenObjectStart
return Delim('{'), nil
case '}':
if dec.tokenState != tokenObjectStart && dec.tokenState != tokenObjectComma {
return dec.tokenError(c)
}
dec.scanp++
dec.tokenState = dec.tokenStack[len(dec.tokenStack)-1]
dec.tokenStack = dec.tokenStack[:len(dec.tokenStack)-1]
dec.tokenValueEnd()
return Delim('}'), nil
case ':':
if dec.tokenState != tokenObjectColon {
return dec.tokenError(c)
}
dec.scanp++
dec.tokenState = tokenObjectValue
continue
case ',':
if dec.tokenState == tokenArrayComma {
dec.scanp++
dec.tokenState = tokenArrayValue
continue
}
if dec.tokenState == tokenObjectComma {
dec.scanp++
dec.tokenState = tokenObjectKey
continue
}
return dec.tokenError(c)
case '"':
if dec.tokenState == tokenObjectStart || dec.tokenState == tokenObjectKey {
var x string
old := dec.tokenState
dec.tokenState = tokenTopValue
err := dec.Decode(&x)
dec.tokenState = old
if err != nil {
return nil, err
}
dec.tokenState = tokenObjectColon
return x, nil
}
fallthrough
default:
if !dec.tokenValueAllowed() {
return dec.tokenError(c)
}
var x any
if err := dec.Decode(&x); err != nil {
return nil, err
}
return x, nil
}
}
}
func (dec *Decoder) tokenError(c byte) (Token, error) {
var context string
switch dec.tokenState {
case tokenTopValue:
context = " looking for beginning of value"
case tokenArrayStart, tokenArrayValue, tokenObjectValue:
context = " looking for beginning of value"
case tokenArrayComma:
context = " after array element"
case tokenObjectKey:
context = " looking for beginning of object key string"
case tokenObjectColon:
context = " after object key"
case tokenObjectComma:
context = " after object key:value pair"
}
return nil, &SyntaxError{"invalid character " + quoteChar(c) + context, dec.InputOffset()}
}
// More reports whether there is another element in the
// current array or object being parsed.
func (dec *Decoder) More() bool {
c, err := dec.peek()
return err == nil && c != ']' && c != '}'
}
func (dec *Decoder) peek() (byte, error) {
var err error
for {
for i := dec.scanp; i < len(dec.buf); i++ {
c := dec.buf[i]
if isSpace(c) {
continue
}
dec.scanp = i
return c, nil
}
// buffer has been scanned, now report any error
if err != nil {
return 0, err
}
err = dec.refill()
}
}
// InputOffset returns the input stream byte offset of the current decoder position.
// The offset gives the location of the end of the most recently returned token
// and the beginning of the next token.
func (dec *Decoder) InputOffset() int64 {
return dec.scanned + int64(dec.scanp)
}
+218
View File
@@ -0,0 +1,218 @@
// Copyright 2016 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 json
import "unicode/utf8"
// safeSet holds the value true if the ASCII character with the given array
// position can be represented inside a JSON string without any further
// escaping.
//
// All values are true except for the ASCII control characters (0-31), the
// double quote ("), and the backslash character ("\").
var safeSet = [utf8.RuneSelf]bool{
' ': true,
'!': true,
'"': false,
'#': true,
'$': true,
'%': true,
'&': true,
'\'': true,
'(': true,
')': true,
'*': true,
'+': true,
',': true,
'-': true,
'.': true,
'/': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
':': true,
';': true,
'<': true,
'=': true,
'>': true,
'?': true,
'@': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'V': true,
'W': true,
'X': true,
'Y': true,
'Z': true,
'[': true,
'\\': false,
']': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'{': true,
'|': true,
'}': true,
'~': true,
'\u007f': true,
}
// htmlSafeSet holds the value true if the ASCII character with the given
// array position can be safely represented inside a JSON string, embedded
// inside of HTML <script> tags, without any additional escaping.
//
// All values are true except for the ASCII control characters (0-31), the
// double quote ("), the backslash character ("\"), HTML opening and closing
// tags ("<" and ">"), and the ampersand ("&").
var htmlSafeSet = [utf8.RuneSelf]bool{
' ': true,
'!': true,
'"': false,
'#': true,
'$': true,
'%': true,
'&': false,
'\'': true,
'(': true,
')': true,
'*': true,
'+': true,
',': true,
'-': true,
'.': true,
'/': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
':': true,
';': true,
'<': false,
'=': true,
'>': false,
'?': true,
'@': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'V': true,
'W': true,
'X': true,
'Y': true,
'Z': true,
'[': true,
'\\': false,
']': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'{': true,
'|': true,
'}': true,
'~': true,
'\u007f': true,
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2011 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 json
import (
"strings"
)
// 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) {
tag, opt, _ := strings.Cut(tag, ",")
return tag, tagOptions(opt)
}
// 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 name string
name, s, _ = strings.Cut(s, ",")
if name == optionName {
return true
}
}
return false
}
+150
View File
@@ -0,0 +1,150 @@
/*
Copyright 2021 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 json
import (
gojson "encoding/json"
"fmt"
"io"
internaljson "sigs.k8s.io/json/internal/golang/encoding/json"
)
// Decoder describes the decoding API exposed by `encoding/json#Decoder`
type Decoder interface {
Decode(v interface{}) error
Buffered() io.Reader
Token() (gojson.Token, error)
More() bool
InputOffset() int64
}
// NewDecoderCaseSensitivePreserveInts returns a decoder that matches the behavior of encoding/json#NewDecoder, with the following changes:
// - When unmarshaling into a struct, JSON keys must case-sensitively match `json` tag names (for tagged struct fields)
// or struct field names (for untagged struct fields), or they are treated as unknown fields and discarded.
// - When unmarshaling a number into an interface value, it is unmarshaled as an int64 if
// the JSON data does not contain a "." character and parses as an integer successfully and
// does not overflow int64. Otherwise, the number is unmarshaled as a float64.
// - If a syntax error is returned, it will not be of type encoding/json#SyntaxError,
// but will be recognizeable by this package's IsSyntaxError() function.
func NewDecoderCaseSensitivePreserveInts(r io.Reader) Decoder {
d := internaljson.NewDecoder(r)
d.CaseSensitive()
d.PreserveInts()
return d
}
// UnmarshalCaseSensitivePreserveInts parses the JSON-encoded data and stores the result in the value pointed to by v.
//
// UnmarshalCaseSensitivePreserveInts matches the behavior of encoding/json#Unmarshal, with the following changes:
// - When unmarshaling into a struct, JSON keys must case-sensitively match `json` tag names (for tagged struct fields)
// or struct field names (for untagged struct fields), or they are treated as unknown fields and discarded.
// - When unmarshaling a number into an interface value, it is unmarshaled as an int64 if
// the JSON data does not contain a "." character and parses as an integer successfully and
// does not overflow int64. Otherwise, the number is unmarshaled as a float64.
// - If a syntax error is returned, it will not be of type encoding/json#SyntaxError,
// but will be recognizeable by this package's IsSyntaxError() function.
func UnmarshalCaseSensitivePreserveInts(data []byte, v interface{}) error {
return internaljson.Unmarshal(
data,
v,
internaljson.CaseSensitive,
internaljson.PreserveInts,
)
}
type StrictOption int
const (
// DisallowDuplicateFields returns strict errors if data contains duplicate fields
DisallowDuplicateFields StrictOption = 1
// DisallowUnknownFields returns strict errors if data contains unknown fields when decoding into typed structs
DisallowUnknownFields StrictOption = 2
)
// UnmarshalStrict parses the JSON-encoded data and stores the result in the value pointed to by v.
// Unmarshaling is performed identically to UnmarshalCaseSensitivePreserveInts(), returning an error on failure.
//
// If parsing succeeds, additional strict checks as selected by `strictOptions` are performed
// and a list of the strict failures (if any) are returned. If no `strictOptions` are selected,
// all supported strict checks are performed.
//
// Strict errors returned will implement the FieldError interface for the specific erroneous fields.
//
// Currently supported strict checks are:
// - DisallowDuplicateFields: ensure the data contains no duplicate fields
// - DisallowUnknownFields: ensure the data contains no unknown fields (when decoding into typed structs)
//
// Additional strict checks may be added in the future.
//
// Note that the strict checks do not change what is stored in v.
// For example, if duplicate fields are present, they will be parsed and stored in v,
// and errors about the duplicate fields will be returned in the strict error list.
func UnmarshalStrict(data []byte, v interface{}, strictOptions ...StrictOption) (strictErrors []error, err error) {
if len(strictOptions) == 0 {
err = internaljson.Unmarshal(data, v,
// options matching UnmarshalCaseSensitivePreserveInts
internaljson.CaseSensitive,
internaljson.PreserveInts,
// all strict options
internaljson.DisallowDuplicateFields,
internaljson.DisallowUnknownFields,
)
} else {
opts := make([]internaljson.UnmarshalOpt, 0, 2+len(strictOptions))
// options matching UnmarshalCaseSensitivePreserveInts
opts = append(opts, internaljson.CaseSensitive, internaljson.PreserveInts)
for _, strictOpt := range strictOptions {
switch strictOpt {
case DisallowDuplicateFields:
opts = append(opts, internaljson.DisallowDuplicateFields)
case DisallowUnknownFields:
opts = append(opts, internaljson.DisallowUnknownFields)
default:
return nil, fmt.Errorf("unknown strict option %d", strictOpt)
}
}
err = internaljson.Unmarshal(data, v, opts...)
}
if strictErr, ok := err.(*internaljson.UnmarshalStrictError); ok {
return strictErr.Errors, nil
}
return nil, err
}
// SyntaxErrorOffset returns if the specified error is a syntax error produced by encoding/json or this package.
func SyntaxErrorOffset(err error) (isSyntaxError bool, offset int64) {
switch err := err.(type) {
case *gojson.SyntaxError:
return true, err.Offset
case *internaljson.SyntaxError:
return true, err.Offset
default:
return false, 0
}
}
// FieldError is an error that provides access to the path of the erroneous field
type FieldError interface {
error
// FieldPath provides the full path of the erroneous field within the json object.
FieldPath() string
// SetFieldPath updates the path of the erroneous field output in the error message.
SetFieldPath(path string)
}
+43
View File
@@ -0,0 +1,43 @@
# Contributing Guidelines
Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://git.k8s.io/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt:
_As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._
## Getting Started
We have full documentation on how to get started contributing here:
<!---
If your repo has certain guidelines for contribution, put them here ahead of the general k8s resources
-->
- [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests
- [Kubernetes Contributor Guide](https://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](https://git.k8s.io/community/contributors/guide#contributing)
- [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet) - Common resources for existing developers
## Mentorship
- [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers!
<!---
Custom Information - if you're copying this template for the first time you can add custom content here, for example:
## Contact Information
- [Slack channel](https://kubernetes.slack.com/messages/kubernetes-users) - Replace `kubernetes-users` with your slack channel string, this will send users directly to your channel.
- [Mailing list](URL)
-->
## Project Management
The [maintainers](https://github.com/kubernetes-sigs/randfill/blob/main/OWNERS_ALIASES#L12) of this project (and often others who have official positions on the [contributor ladder](https://github.com/kubernetes-sigs/randfill/blob/main/OWNERS_ALIASES)) are responsible for performing project management which oversees development and maintenance of the API, tests, tools, e.t.c. While we try to be generally flexible when it comes to the management of individual pieces (such as Issues or PRs), we have some rules and guidelines which help us plan, coordinate and reduce waste. In this section you'll find some rules/guidelines for contributors related to project management which may extend or go beyond what you would find in the standard [Kubernetes Contributor Guide](https://git.k8s.io/community/contributors/guide).
### Bumping stale and closed Issues & PRs
Maintainers are ultimately responsible for triaging new issues and PRs, accepting or declining them, deciding priority and fitting them into milestones intended for future releases. Bots are responsible for marking issues and PRs which stagnate as stale, or closing them if progress does not continue for a long period of time. Due to the nature of this community-driven development effort (we do not have dedicated engineering resources, we rely on the community which is effectively "volunteer time") **not all issues can be accepted, prioritized or completed**.
You may find times when an issue you're subscribed to and interested in seems to stagnate, or perhaps gets auto-closed. Prior to bumping or directly re-opening issues yourself, we generally ask that you bring these up for discussion on the agenda for one of our community syncs if possible, or bring them up for discussion in Slack or the mailing list as this gives us a better opportunity to discuss the issue and determine viability and logistics. If feasible we **highly recommend being ready to contribute directly** to any stale or unprioritized effort that you want to see move forward, as **the best way to ensure progress is to engage with the community and personally invest time**.
We (the community) aren't opposed to making exceptions in some cases, but when in doubt please follow the above guidelines before bumping closed or stale issues if you're not ready to personally invest time in them. We are responsible for managing these and without further context or engagement we may set these back to how they were previously organized.
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 The gofuzz Authors
Copyright 2025 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.
+24
View File
@@ -0,0 +1,24 @@
When donating the randfill project to the CNCF, we could not reach all the
gofuzz contributors to sign the CNCF CLA. As such, according to the CNCF rules
to donate a repository, we must add a NOTICE referencing section 7 of the CLA
with a list of developers who could not be reached.
`7. Should You wish to submit work that is not Your original creation, You may
submit it to the Foundation separately from any Contribution, identifying the
complete details of its source and of any license or other restriction
(including, but not limited to, related patents, trademarks, and license
agreements) of which you are personally aware, and conspicuously marking the
work as "Submitted on behalf of a third-party: [named here]".`
Submitted on behalf of a third-party: @dnephin (Daniel Nephin)
Submitted on behalf of a third-party: @AlekSi (Alexey Palazhchenko)
Submitted on behalf of a third-party: @bbigras (Bruno Bigras)
Submitted on behalf of a third-party: @samirkut (Samir)
Submitted on behalf of a third-party: @posener (Eyal Posener)
Submitted on behalf of a third-party: @Ashikpaul (Ashik Paul)
Submitted on behalf of a third-party: @kwongtailau (Kwongtai)
Submitted on behalf of a third-party: @ericcornelissen (Eric Cornelissen)
Submitted on behalf of a third-party: @eclipseo (Robert-André Mauchin)
Submitted on behalf of a third-party: @yanzhoupan (Andrew Pan)
Submitted on behalf of a third-party: @STRRL (Zhiqiang ZHOU)
Submitted on behalf of a third-party: @disconnect3d (Disconnect3d)
+8
View File
@@ -0,0 +1,8 @@
# See the OWNERS docs at https://go.k8s.io/owners
# See the OWNERS_ALIASES file at https://github.com/kubernetes-sigs/randfill/blob/main/OWNERS_ALIASES for a list of members for each alias.
approvers:
- sig-testing-leads
- thockin
reviewers: []
+14
View File
@@ -0,0 +1,14 @@
# See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md
# This file should be kept in sync with k/org.
aliases:
# Reference: https://github.com/kubernetes/org/blob/main/OWNERS_ALIASES
sig-testing-leads:
- BenTheElder
- alvaroaleman
- aojea
- cjwagner
- jbpratt
- michelle192837
- pohly
- xmcqueen
+98
View File
@@ -0,0 +1,98 @@
randfill
======
randfill is a library for populating go objects with random values.
This is a fork of github.com/google/gofuzz, which was archived.
NOTE: This repo is supported only for use within Kubernetes. It is not our
intention to support general use. That said, if it works for you, that's
great! If you have a problem, please feel free to file an issue, but be aware
that it may not be a priority for us to fix it unless it is affecting
Kubernetes. PRs are welcome, within reason.
[![GoDoc](https://godoc.org/sigs.k8s.io/randfill?status.svg)](https://godoc.org/sigs.k8s.io/randfill)
This is useful for testing:
* Do your project's objects really serialize/unserialize correctly in all cases?
* Is there an incorrectly formatted object that will cause your project to panic?
Import with ```import "sigs.k8s.io/randfill"```
You can use it on single variables:
```go
f := randfill.New()
var myInt int
f.Fill(&myInt) // myInt gets a random value.
```
You can use it on maps:
```go
f := randfill.New().NilChance(0).NumElements(1, 1)
var myMap map[ComplexKeyType]string
f.Fill(&myMap) // myMap will have exactly one element.
```
Customize the chance of getting a nil pointer:
```go
f := randfill.New().NilChance(.5)
var fancyStruct struct {
A, B, C, D *string
}
f.Fill(&fancyStruct) // About half the pointers should be set.
```
You can even customize the randomization completely if needed:
```go
type MyEnum string
const (
A MyEnum = "A"
B MyEnum = "B"
)
type MyInfo struct {
Type MyEnum
AInfo *string
BInfo *string
}
f := randfill.New().NilChance(0).Funcs(
func(e *MyInfo, c randfill.Continue) {
switch c.Intn(2) {
case 0:
e.Type = A
c.Fill(&e.AInfo)
case 1:
e.Type = B
c.Fill(&e.BInfo)
}
},
)
var myObject MyInfo
f.Fill(&myObject) // Type will correspond to whether A or B info is set.
```
See more examples in ```example_test.go```.
## dvyukov/go-fuzz integration
You can use this library for easier [go-fuzz](https://github.com/dvyukov/go-fuzz)ing.
go-fuzz provides the user a byte-slice, which should be converted to different inputs
for the tested function. This library can help convert the byte slice. Consider for
example a fuzz test for a the function `mypackage.MyFunc` that takes an int arguments:
```go
// +build gofuzz
package mypackage
import "sigs.k8s.io/randfill"
func Fuzz(data []byte) int {
var i int
randfill.NewFromGoFuzz(data).Fill(&i)
MyFunc(i)
return 0
}
```
Happy testing!
+16
View File
@@ -0,0 +1,16 @@
# Defined below are the security contacts for this repo.
#
# They are the contact point for the Product Security Committee to reach out
# to for triaging and handling of incoming issues.
#
# The below names agree to abide by the
# [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy)
# and will be removed and replaced if they violate that agreement.
#
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
# INSTRUCTIONS AT https://kubernetes.io/security/
thockin
BenTheElder
aojea
pohly
+81
View File
@@ -0,0 +1,81 @@
/*
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 bytesource provides a rand.Source64 that is determined by a slice of bytes.
package bytesource
import (
"bytes"
"encoding/binary"
"io"
"math/rand"
)
// ByteSource implements rand.Source64 determined by a slice of bytes. The random numbers are
// generated from each 8 bytes in the slice, until the last bytes are consumed, from which a
// fallback pseudo random source is created in case more random numbers are required.
// It also exposes a `bytes.Reader` API, which lets callers consume the bytes directly.
type ByteSource struct {
*bytes.Reader
fallback rand.Source
}
// New returns a new ByteSource from a given slice of bytes.
func New(input []byte) *ByteSource {
s := &ByteSource{
Reader: bytes.NewReader(input),
fallback: rand.NewSource(0),
}
if len(input) > 0 {
s.fallback = rand.NewSource(int64(s.consumeUint64()))
}
return s
}
func (s *ByteSource) Uint64() uint64 {
// Return from input if it was not exhausted.
if s.Len() > 0 {
return s.consumeUint64()
}
// Input was exhausted, return random number from fallback (in this case fallback should not be
// nil). Try first having a Uint64 output (Should work in current rand implementation),
// otherwise return a conversion of Int63.
if s64, ok := s.fallback.(rand.Source64); ok {
return s64.Uint64()
}
return uint64(s.fallback.Int63())
}
func (s *ByteSource) Int63() int64 {
return int64(s.Uint64() >> 1)
}
func (s *ByteSource) Seed(seed int64) {
s.fallback = rand.NewSource(seed)
s.Reader = bytes.NewReader(nil)
}
// consumeUint64 reads 8 bytes from the input and convert them to a uint64. It assumes that the the
// bytes reader is not empty.
func (s *ByteSource) consumeUint64() uint64 {
var bytes [8]byte
_, err := s.Read(bytes[:])
if err != nil && err != io.EOF {
panic("failed reading source") // Should not happen.
}
return binary.BigEndian.Uint64(bytes[:])
}
+3
View File
@@ -0,0 +1,3 @@
# Kubernetes Community Code of Conduct
Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md)
+682
View File
@@ -0,0 +1,682 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Copyright 2014 The gofuzz Authors.
Copyright 2025 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 randfill is a library for populating go objects with random values.
package randfill
import (
"fmt"
"math/rand"
"reflect"
"regexp"
"sync"
"time"
"unsafe"
"strings"
"sigs.k8s.io/randfill/bytesource"
)
// funcMap is a map from a type to a function that randfills that type. The
// function is a reflect.Value because the type being filled is different for
// each func.
type funcMap map[reflect.Type]reflect.Value
// Filler knows how to fill any object with random fields.
type Filler struct {
customFuncs funcMap
defaultFuncs funcMap
r *rand.Rand
nilChance float64
minElements int
maxElements int
maxDepth int
allowUnexportedFields bool
skipFieldPatterns []*regexp.Regexp
lock sync.Mutex
}
// New returns a new Filler. Customize your Filler further by calling Funcs,
// RandSource, NilChance, or NumElements in any order.
func New() *Filler {
return NewWithSeed(time.Now().UnixNano())
}
func NewWithSeed(seed int64) *Filler {
f := &Filler{
defaultFuncs: funcMap{
reflect.TypeOf(&time.Time{}): reflect.ValueOf(randfillTime),
},
customFuncs: funcMap{},
r: rand.New(rand.NewSource(seed)),
nilChance: .2,
minElements: 1,
maxElements: 10,
maxDepth: 100,
allowUnexportedFields: false,
}
return f
}
// NewFromGoFuzz is a helper function that enables using randfill (this
// project) with go-fuzz (https://github.com/dvyukov/go-fuzz) for continuous
// fuzzing. Essentially, it enables translating the fuzzing bytes from
// go-fuzz to any Go object using this library.
//
// This implementation promises a constant translation from a given slice of
// bytes to the fuzzed objects. This promise will remain over future
// versions of Go and of this library.
//
// Note: the returned Filler should not be shared between multiple goroutines,
// as its deterministic output will no longer be available.
//
// Example: use go-fuzz to test the function `MyFunc(int)` in the package
// `mypackage`. Add the file: "mypackage_fuzz.go" with the content:
//
// // +build gofuzz
// package mypackage
// import "sigs.k8s.io/randfill"
//
// func Fuzz(data []byte) int {
// var i int
// randfill.NewFromGoFuzz(data).Fill(&i)
// MyFunc(i)
// return 0
// }
func NewFromGoFuzz(data []byte) *Filler {
return New().RandSource(bytesource.New(data))
}
// Funcs registers custom fill functions for this Filler.
//
// Each entry in customFuncs must be a function taking two parameters.
// The first parameter must be a pointer or map. It is the variable that
// function will fill with random data. The second parameter must be a
// randfill.Continue, which will provide a source of randomness and a way
// to automatically continue filling smaller pieces of the first parameter.
//
// These functions are called sensibly, e.g., if you wanted custom string
// filling, the function `func(s *string, c randfill.Continue)` would get
// called and passed the address of strings. Maps and pointers will always
// be made/new'd for you, ignoring the NilChance option. For slices, it
// doesn't make much sense to pre-create them--Filler doesn't know how
// long you want your slice--so take a pointer to a slice, and make it
// yourself. (If you don't want your map/pointer type pre-made, take a
// pointer to it, and make it yourself.) See the examples for a range of
// custom functions.
//
// If a function is already registered for a type, and a new function is
// provided, the previous function will be replaced with the new one.
func (f *Filler) Funcs(customFuncs ...interface{}) *Filler {
for i := range customFuncs {
v := reflect.ValueOf(customFuncs[i])
if v.Kind() != reflect.Func {
panic("Filler.Funcs: all arguments must be functions")
}
t := v.Type()
if t.NumIn() != 2 || t.NumOut() != 0 {
panic("Filler.Funcs: all customFuncs must have 2 arguments and 0 returns")
}
argT := t.In(0)
switch argT.Kind() {
case reflect.Ptr, reflect.Map:
default:
panic("Filler.Funcs: customFuncs' first argument must be a pointer or map type")
}
if t.In(1) != reflect.TypeOf(Continue{}) {
panic("Filler.Funcs: customFuncs' second argument must be a randfill.Continue")
}
f.customFuncs[argT] = v
}
return f
}
// RandSource causes this Filler to get values from the given source of
// randomness. Use this if you want deterministic filling.
func (f *Filler) RandSource(s rand.Source) *Filler {
f.r = rand.New(s)
return f
}
// NilChance sets the probability of creating a nil pointer, map, or slice to
// 'p'. 'p' should be between 0 (no nils) and 1 (all nils), inclusive.
func (f *Filler) NilChance(p float64) *Filler {
if p < 0 || p > 1 {
panic("Filler.NilChance: p must be between 0 and 1, inclusive")
}
f.nilChance = p
return f
}
// NumElements sets the minimum and maximum number of elements that will be
// added to a non-nil map or slice.
func (f *Filler) NumElements(min, max int) *Filler {
if min < 0 {
panic("Filler.NumElements: min must be >= 0")
}
if min > max {
panic("Filler.NumElements: min must be <= max")
}
f.minElements = min
f.maxElements = max
return f
}
func (f *Filler) genElementCount() int {
if f.minElements == f.maxElements {
return f.minElements
}
return f.minElements + f.r.Intn(f.maxElements-f.minElements+1)
}
func (f *Filler) genShouldFill() bool {
return f.r.Float64() >= f.nilChance
}
// MaxDepth sets the maximum number of recursive fill calls that will be made
// before stopping. This includes struct members, pointers, and map and slice
// elements.
func (f *Filler) MaxDepth(d int) *Filler {
f.maxDepth = d
return f
}
// AllowUnexportedFields defines whether to fill unexported fields.
func (f *Filler) AllowUnexportedFields(flag bool) *Filler {
f.allowUnexportedFields = flag
return f
}
// SkipFieldsWithPattern tells this Filler to skip any field whose name matches
// the supplied pattern. Call this multiple times if needed. This is useful to
// skip XXX_ fields generated by protobuf.
func (f *Filler) SkipFieldsWithPattern(pattern *regexp.Regexp) *Filler {
f.skipFieldPatterns = append(f.skipFieldPatterns, pattern)
return f
}
// SimpleSelfFiller represents an object that knows how to randfill itself.
//
// Unlike NativeSelfFiller, this interface does not cause the type in question
// to depend on the randfill package. This is most useful for simple types. For
// more complex types, consider using NativeSelfFiller.
type SimpleSelfFiller interface {
// RandFill fills the current object with random data.
RandFill(r *rand.Rand)
}
// NativeSelfFiller represents an object that knows how to randfill itself.
//
// Unlike SimpleSelfFiller, this interface allows for recursive filling of
// child objects with the same rules as the parent Filler.
type NativeSelfFiller interface {
// RandFill fills the current object with random data.
RandFill(c Continue)
}
// Fill recursively fills all of obj's fields with something random. First
// this tries to find a custom fill function (see Funcs). If there is no
// custom function, this tests whether the object implements SimpleSelfFiller
// or NativeSelfFiller and if so, calls RandFill on it to fill itself. If that
// fails, this will see if there is a default fill function provided by this
// package. If all of that fails, this will generate random values for all
// primitive fields and then recurse for all non-primitives.
//
// This is safe for cyclic or tree-like structs, up to a limit. Use the
// MaxDepth method to adjust how deep you need it to recurse.
//
// obj must be a pointer. Exported (public) fields can always be set, and if
// the AllowUnexportedFields() modifier was called it can try to set unexported
// (private) fields, too.
//
// This is intended for tests, so will panic on bad input or unimplemented
// types. This method takes a lock for the whole Filler, so it is not
// reentrant. See Continue.
func (f *Filler) Fill(obj interface{}) {
f.lock.Lock()
defer f.lock.Unlock()
v := reflect.ValueOf(obj)
if v.Kind() != reflect.Ptr {
panic("Filler.Fill: obj must be a pointer")
}
v = v.Elem()
f.fillWithContext(v, 0)
}
// FillNoCustom is just like Fill, except that any custom fill function for
// obj's type will not be called and obj will not be tested for
// SimpleSelfFiller or NativeSelfFiller. This applies only to obj and not other
// instances of obj's type or to obj's child fields.
//
// obj must be a pointer. Exported (public) fields can always be set, and if
// the AllowUnexportedFields() modifier was called it can try to set unexported
// (private) fields, too.
//
// This is intended for tests, so will panic on bad input or unimplemented
// types. This method takes a lock for the whole Filler, so it is not
// reentrant. See Continue.
func (f *Filler) FillNoCustom(obj interface{}) {
f.lock.Lock()
defer f.lock.Unlock()
v := reflect.ValueOf(obj)
if v.Kind() != reflect.Ptr {
panic("Filler.FillNoCustom: obj must be a pointer")
}
v = v.Elem()
f.fillWithContext(v, flagNoCustomFill)
}
const (
// Do not try to find a custom fill function. Does not apply recursively.
flagNoCustomFill uint64 = 1 << iota
)
func (f *Filler) fillWithContext(v reflect.Value, flags uint64) {
fc := &fillerContext{filler: f}
fc.doFill(v, flags)
}
// fillerContext carries context about a single filling run, which lets Filler
// be thread-safe.
type fillerContext struct {
filler *Filler
curDepth int
}
func (fc *fillerContext) doFill(v reflect.Value, flags uint64) {
if fc.curDepth >= fc.filler.maxDepth {
return
}
fc.curDepth++
defer func() { fc.curDepth-- }()
if !v.CanSet() {
if !fc.filler.allowUnexportedFields || !v.CanAddr() {
return
}
v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem()
}
if flags&flagNoCustomFill == 0 {
// Check for both pointer and non-pointer custom functions.
if v.CanAddr() && fc.tryCustom(v.Addr()) {
return
}
if fc.tryCustom(v) {
return
}
}
if fn, ok := fillFuncMap[v.Kind()]; ok {
fn(v, fc.filler.r)
return
}
switch v.Kind() {
case reflect.Map:
if fc.filler.genShouldFill() {
v.Set(reflect.MakeMap(v.Type()))
n := fc.filler.genElementCount()
for i := 0; i < n; i++ {
key := reflect.New(v.Type().Key()).Elem()
fc.doFill(key, 0)
val := reflect.New(v.Type().Elem()).Elem()
fc.doFill(val, 0)
v.SetMapIndex(key, val)
}
return
}
v.Set(reflect.Zero(v.Type()))
case reflect.Ptr:
if fc.filler.genShouldFill() {
v.Set(reflect.New(v.Type().Elem()))
fc.doFill(v.Elem(), 0)
return
}
v.Set(reflect.Zero(v.Type()))
case reflect.Slice:
if fc.filler.genShouldFill() {
n := fc.filler.genElementCount()
v.Set(reflect.MakeSlice(v.Type(), n, n))
for i := 0; i < n; i++ {
fc.doFill(v.Index(i), 0)
}
return
}
v.Set(reflect.Zero(v.Type()))
case reflect.Array:
if fc.filler.genShouldFill() {
n := v.Len()
for i := 0; i < n; i++ {
fc.doFill(v.Index(i), 0)
}
return
}
v.Set(reflect.Zero(v.Type()))
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
skipField := false
fieldName := v.Type().Field(i).Name
for _, pattern := range fc.filler.skipFieldPatterns {
if pattern.MatchString(fieldName) {
skipField = true
break
}
}
if !skipField {
fc.doFill(v.Field(i), 0)
}
}
case reflect.Chan:
fallthrough
case reflect.Func:
fallthrough
case reflect.Interface:
fallthrough
default:
panic(fmt.Sprintf("can't fill type %v, kind %v", v.Type(), v.Kind()))
}
}
// tryCustom searches for custom handlers, and returns true iff it finds a match
// and successfully randomizes v.
func (fc *fillerContext) tryCustom(v reflect.Value) bool {
// First: see if we have a fill function for it.
doCustom, ok := fc.filler.customFuncs[v.Type()]
if !ok {
// Second: see if it can fill itself.
if v.CanInterface() {
intf := v.Interface()
if fillable, ok := intf.(SimpleSelfFiller); ok {
fillable.RandFill(fc.filler.r)
return true
}
if fillable, ok := intf.(NativeSelfFiller); ok {
fillable.RandFill(Continue{fc: fc, Rand: fc.filler.r})
return true
}
}
// Finally: see if there is a default fill function.
doCustom, ok = fc.filler.defaultFuncs[v.Type()]
if !ok {
return false
}
}
switch v.Kind() {
case reflect.Ptr:
if v.IsNil() {
if !v.CanSet() {
return false
}
v.Set(reflect.New(v.Type().Elem()))
}
case reflect.Map:
if v.IsNil() {
if !v.CanSet() {
return false
}
v.Set(reflect.MakeMap(v.Type()))
}
default:
return false
}
doCustom.Call([]reflect.Value{
v,
reflect.ValueOf(Continue{
fc: fc,
Rand: fc.filler.r,
}),
})
return true
}
// Continue can be passed to custom fill functions to allow them to use
// the correct source of randomness and to continue filling their members.
type Continue struct {
fc *fillerContext
// For convenience, Continue implements rand.Rand via embedding.
// Use this for generating any randomness if you want your filling
// to be repeatable for a given seed.
*rand.Rand
}
// Fill continues filling obj. obj must be a pointer or a reflect.Value of a
// pointer. See Filler.Fill.
func (c Continue) Fill(obj interface{}) {
v, ok := obj.(reflect.Value)
if !ok {
v = reflect.ValueOf(obj)
}
if v.Kind() != reflect.Ptr {
panic("Continue.Fill: obj must be a pointer")
}
v = v.Elem()
c.fc.doFill(v, 0)
}
// FillNoCustom continues filling obj, except that any custom fill function for
// obj's type will not be called and obj will not be tested for
// SimpleSelfFiller or NativeSelfFiller. See Filler.FillNoCustom.
func (c Continue) FillNoCustom(obj interface{}) {
v, ok := obj.(reflect.Value)
if !ok {
v = reflect.ValueOf(obj)
}
if v.Kind() != reflect.Ptr {
panic("Continue.FillNoCustom: obj must be a pointer")
}
v = v.Elem()
c.fc.doFill(v, flagNoCustomFill)
}
const defaultStringMaxLen = 20
// String makes a random string up to n characters long. If n is 0, the default
// size range is [0-20). The returned string may include a variety of (valid)
// UTF-8 encodings.
func (c Continue) String(n int) string {
return randString(c.Rand, n)
}
// Uint64 makes random 64 bit numbers.
// Weirdly, rand doesn't have a function that gives you 64 random bits.
func (c Continue) Uint64() uint64 {
return randUint64(c.Rand)
}
// Bool returns true or false randomly.
func (c Continue) Bool() bool {
return randBool(c.Rand)
}
func fillInt(v reflect.Value, r *rand.Rand) {
v.SetInt(int64(randUint64(r)))
}
func fillUint(v reflect.Value, r *rand.Rand) {
v.SetUint(randUint64(r))
}
func randfillTime(t *time.Time, c Continue) {
var sec, nsec int64
// Allow for about 1000 years of random time values, which keeps things
// like JSON parsing reasonably happy.
sec = c.Rand.Int63n(1000 * 365 * 24 * 60 * 60)
// Nanosecond values greater than 1Bn are technically allowed but result in
// time.Time values with invalid timezone offsets.
nsec = c.Rand.Int63n(999999999)
*t = time.Unix(sec, nsec)
}
var fillFuncMap = map[reflect.Kind]func(reflect.Value, *rand.Rand){
reflect.Bool: func(v reflect.Value, r *rand.Rand) {
v.SetBool(randBool(r))
},
reflect.Int: fillInt,
reflect.Int8: fillInt,
reflect.Int16: fillInt,
reflect.Int32: fillInt,
reflect.Int64: fillInt,
reflect.Uint: fillUint,
reflect.Uint8: fillUint,
reflect.Uint16: fillUint,
reflect.Uint32: fillUint,
reflect.Uint64: fillUint,
reflect.Uintptr: fillUint,
reflect.Float32: func(v reflect.Value, r *rand.Rand) {
v.SetFloat(float64(r.Float32()))
},
reflect.Float64: func(v reflect.Value, r *rand.Rand) {
v.SetFloat(r.Float64())
},
reflect.Complex64: func(v reflect.Value, r *rand.Rand) {
v.SetComplex(complex128(complex(r.Float32(), r.Float32())))
},
reflect.Complex128: func(v reflect.Value, r *rand.Rand) {
v.SetComplex(complex(r.Float64(), r.Float64()))
},
reflect.String: func(v reflect.Value, r *rand.Rand) {
v.SetString(randString(r, 0))
},
reflect.UnsafePointer: func(v reflect.Value, r *rand.Rand) {
panic("filling of UnsafePointers is not implemented")
},
}
// randBool returns true or false randomly.
func randBool(r *rand.Rand) bool {
return r.Int31()&(1<<30) == 0
}
type int63nPicker interface {
Int63n(int64) int64
}
// UnicodeRange describes a sequential range of unicode characters.
// Last must be numerically greater than First.
type UnicodeRange struct {
First, Last rune
}
// UnicodeRanges describes an arbitrary number of sequential ranges of unicode characters.
// To be useful, each range must have at least one character (First <= Last) and
// there must be at least one range.
type UnicodeRanges []UnicodeRange
// choose returns a random unicode character from the given range, using the
// given randomness source.
func (ur UnicodeRange) choose(r int63nPicker) rune {
count := int64(ur.Last - ur.First + 1)
return ur.First + rune(r.Int63n(count))
}
// CustomStringFillFunc constructs a FillFunc which produces random strings.
// Each character is selected from the range ur. If there are no characters
// in the range (cr.Last < cr.First), this will panic.
func (ur UnicodeRange) CustomStringFillFunc(n int) func(s *string, c Continue) {
ur.check()
return func(s *string, c Continue) {
*s = ur.randString(c.Rand, n)
}
}
// check is a function that used to check whether the first of ur(UnicodeRange)
// is greater than the last one.
func (ur UnicodeRange) check() {
if ur.Last < ur.First {
panic("UnicodeRange.check: the last encoding must be greater than the first")
}
}
// randString of UnicodeRange makes a random string up to 20 characters long.
// Each character is selected form ur(UnicodeRange).
func (ur UnicodeRange) randString(r *rand.Rand, max int) string {
if max == 0 {
max = defaultStringMaxLen
}
n := r.Intn(max)
sb := strings.Builder{}
sb.Grow(n)
for i := 0; i < n; i++ {
sb.WriteRune(ur.choose(r))
}
return sb.String()
}
// defaultUnicodeRanges sets a default unicode range when users do not set
// CustomStringFillFunc() but want to fill strings.
var defaultUnicodeRanges = UnicodeRanges{
{' ', '~'}, // ASCII characters
{'\u00a0', '\u02af'}, // Multi-byte encoded characters
{'\u4e00', '\u9fff'}, // Common CJK (even longer encodings)
}
// CustomStringFillFunc constructs a FillFunc which produces random strings.
// Each character is selected from one of the ranges of ur(UnicodeRanges).
// Each range has an equal probability of being chosen. If there are no ranges,
// or a selected range has no characters (.Last < .First), this will panic.
// Do not modify any of the ranges in ur after calling this function.
func (ur UnicodeRanges) CustomStringFillFunc(n int) func(s *string, c Continue) {
// Check unicode ranges slice is empty.
if len(ur) == 0 {
panic("UnicodeRanges is empty")
}
// if not empty, each range should be checked.
for i := range ur {
ur[i].check()
}
return func(s *string, c Continue) {
*s = ur.randString(c.Rand, n)
}
}
// randString of UnicodeRanges makes a random string up to 20 characters long.
// Each character is selected form one of the ranges of ur(UnicodeRanges),
// and each range has an equal probability of being chosen.
func (ur UnicodeRanges) randString(r *rand.Rand, max int) string {
if max == 0 {
max = defaultStringMaxLen
}
n := r.Intn(max)
sb := strings.Builder{}
sb.Grow(n)
for i := 0; i < n; i++ {
sb.WriteRune(ur[r.Intn(len(ur))].choose(r))
}
return sb.String()
}
// randString makes a random string up to 20 characters long. The returned string
// may include a variety of (valid) UTF-8 encodings.
func randString(r *rand.Rand, max int) string {
return defaultUnicodeRanges.randString(r, max)
}
// randUint64 makes random 64 bit numbers.
// Weirdly, rand doesn't have a function that gives you 64 random bits.
func randUint64(r *rand.Rand) uint64 {
return uint64(r.Uint32())<<32 | uint64(r.Uint32())
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.
+21
View File
@@ -0,0 +1,21 @@
/*
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
+388
View File
@@ -0,0 +1,388 @@
/*
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"
"iter"
"sort"
"strings"
"sigs.k8s.io/structured-merge-diff/v6/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. They are sorted alphabetically.
Key *value.FieldList
// 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
}
// FieldNameElement creates a new FieldName PathElement.
func FieldNameElement(name string) PathElement {
return PathElement{FieldName: &name}
}
// KeyElement creates a new Key PathElement with the key fields.
func KeyElement(fields ...value.Field) PathElement {
l := value.FieldList(fields)
return PathElement{Key: &l}
}
// KeyElementByFields creates a new Key PathElement from names and values.
// `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 KeyElementByFields(nameValues ...any) PathElement {
return PathElement{Key: KeyByFields(nameValues...)}
}
// ValueElement creates a new Value PathElement.
func ValueElement(value value.Value) PathElement {
return PathElement{Value: &value}
}
// IndexElement creates a new Index PathElement.
func IndexElement(index int) PathElement {
return PathElement{Index: &index}
}
// Less provides an order for path elements.
func (e PathElement) Less(rhs PathElement) bool {
return e.Compare(rhs) < 0
}
// Compare provides an order for path elements.
func (e PathElement) Compare(rhs PathElement) int {
if e.FieldName != nil {
if rhs.FieldName == nil {
return -1
}
return strings.Compare(*e.FieldName, *rhs.FieldName)
} else if rhs.FieldName != nil {
return 1
}
if e.Key != nil {
if rhs.Key == nil {
return -1
}
return e.Key.Compare(*rhs.Key)
} else if rhs.Key != nil {
return 1
}
if e.Value != nil {
if rhs.Value == nil {
return -1
}
return value.Compare(*e.Value, *rhs.Value)
} else if rhs.Value != nil {
return 1
}
if e.Index != nil {
if rhs.Index == nil {
return -1
}
if *e.Index < *rhs.Index {
return -1
} else if *e.Index == *rhs.Index {
return 0
}
return 1
} else if rhs.Index != nil {
return 1
}
return 0
}
// Equals returns true if both path elements are equal.
func (e PathElement) Equals(rhs PathElement) bool {
if e.FieldName != nil {
if rhs.FieldName == nil {
return false
}
return *e.FieldName == *rhs.FieldName
} else if rhs.FieldName != nil {
return false
}
if e.Key != nil {
if rhs.Key == nil {
return false
}
return e.Key.Equals(*rhs.Key)
} else if rhs.Key != nil {
return false
}
if e.Value != nil {
if rhs.Value == nil {
return false
}
return value.Equals(*e.Value, *rhs.Value)
} else if rhs.Value != nil {
return false
}
if e.Index != nil {
if rhs.Index == nil {
return false
}
return *e.Index == *rhs.Index
} else if rhs.Index != nil {
return false
}
return true
}
// String presents the path element as a human-readable string.
func (e PathElement) String() string {
switch {
case e.FieldName != nil:
return "." + *e.FieldName
case e.Key != nil:
strs := make([]string, len(*e.Key))
for i, k := range *e.Key {
strs[i] = fmt.Sprintf("%v=%v", k.Name, value.ToString(k.Value))
}
// Keys are supposed to be sorted.
return "[" + strings.Join(strs, ",") + "]"
case e.Value != nil:
return fmt.Sprintf("[=%v]", value.ToString(*e.Value))
case e.Index != nil:
return fmt.Sprintf("[%v]", *e.Index)
default:
return "{{invalid path element}}"
}
}
// Copy returns a copy of the PathElement.
// This is not a full deep copy as any contained value.Value is not copied.
func (e PathElement) Copy() PathElement {
if e.FieldName != nil {
return PathElement{FieldName: e.FieldName}
}
if e.Key != nil {
c := e.Key.Copy()
return PathElement{Key: &c}
}
if e.Value != nil {
return PathElement{Value: e.Value}
}
if e.Index != nil {
return PathElement{Index: e.Index}
}
return e // zero value
}
// 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.FieldList {
if len(nameValues)%2 != 0 {
panic("must have a value for every name")
}
out := value.FieldList{}
for i := 0; i < len(nameValues)-1; i += 2 {
out = append(out, value.Field{Name: nameValues[i].(string), Value: value.NewValueInterface(nameValues[i+1])})
}
out.Sort()
return &out
}
// PathElementSet is a set of path elements.
// TODO: serialize as a list.
type PathElementSet struct {
members sortedPathElements
}
func MakePathElementSet(size int) PathElementSet {
return PathElementSet{
members: make(sortedPathElements, 0, size),
}
}
type sortedPathElements []PathElement
// Implement the sort interface; this would permit bulk creation, which would
// be faster than doing it one at a time via Insert.
func (spe sortedPathElements) Len() int { return len(spe) }
func (spe sortedPathElements) Less(i, j int) bool { return spe[i].Less(spe[j]) }
func (spe sortedPathElements) Swap(i, j int) { spe[i], spe[j] = spe[j], spe[i] }
// Copy returns a copy of the PathElementSet.
// This is not a full deep copy as any contained value.Value is not copied.
func (s PathElementSet) Copy() PathElementSet {
out := make(sortedPathElements, len(s.members))
for i := range s.members {
out[i] = s.members[i].Copy()
}
return PathElementSet{members: out}
}
// Insert adds pe to the set.
func (s *PathElementSet) Insert(pe PathElement) {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].Less(pe)
})
if loc == len(s.members) {
s.members = append(s.members, pe)
return
}
if s.members[loc].Equals(pe) {
return
}
s.members = append(s.members, PathElement{})
copy(s.members[loc+1:], s.members[loc:])
s.members[loc] = pe
}
// Union returns a set containing elements that appear in either s or s2.
func (s *PathElementSet) Union(s2 *PathElementSet) *PathElementSet {
out := &PathElementSet{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.members) {
if s.members[i].Less(s2.members[j]) {
out.members = append(out.members, s.members[i])
i++
} else {
out.members = append(out.members, s2.members[j])
if !s2.members[j].Less(s.members[i]) {
i++
}
j++
}
}
if i < len(s.members) {
out.members = append(out.members, s.members[i:]...)
}
if j < len(s2.members) {
out.members = append(out.members, s2.members[j:]...)
}
return out
}
// Intersection returns a set containing elements which appear in both s and s2.
func (s *PathElementSet) Intersection(s2 *PathElementSet) *PathElementSet {
out := &PathElementSet{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.members) {
if s.members[i].Less(s2.members[j]) {
i++
} else {
if !s2.members[j].Less(s.members[i]) {
out.members = append(out.members, s.members[i])
i++
}
j++
}
}
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{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.members) {
if s.members[i].Less(s2.members[j]) {
out.members = append(out.members, s.members[i])
i++
} else {
if !s2.members[j].Less(s.members[i]) {
i++
}
j++
}
}
if i < len(s.members) {
out.members = append(out.members, s.members[i:]...)
}
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 {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].Less(pe)
})
if loc == len(s.members) {
return false
}
if s.members[loc].Equals(pe) {
return true
}
return false
}
// 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 !s.members[k].Equals(s2.members[k]) {
return false
}
}
return true
}
// Iterate calls f for each PathElement in the set. The order is deterministic.
func (s *PathElementSet) Iterate(f func(PathElement)) {
for _, pe := range s.members {
f(pe)
}
}
// All iterates over each PathElement in the set. The order is deterministic.
func (s *PathElementSet) All() iter.Seq[PathElement] {
return func(yield func(element PathElement) bool) {
for _, pe := range s.members {
if !yield(pe) {
return
}
}
}
}
+134
View File
@@ -0,0 +1,134 @@
/*
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/v6/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,
allocator: value.NewFreelistAllocator(),
do: func(p Path) { s.Insert(p) },
}
w.walk()
return s
}
type objectWalker struct {
path Path
value value.Value
allocator value.Allocator
do func(Path)
}
func (w *objectWalker) walk() {
switch {
case w.value.IsNull():
case w.value.IsFloat():
case w.value.IsInt():
case w.value.IsString():
case w.value.IsBool():
// All leaf fields handled the same way (after the switch
// statement).
// Descend
case w.value.IsList():
// If the list were atomic, we'd break here, but we don't have
// a schema, so we can't tell.
l := w.value.AsListUsing(w.allocator)
defer w.allocator.Free(l)
iter := l.RangeUsing(w.allocator)
defer w.allocator.Free(iter)
for iter.Next() {
i, value := iter.Item()
w2 := *w
w2.path = append(w.path, w.GuessBestListPathElement(i, value))
w2.value = value
w2.walk()
}
return
case w.value.IsMap():
// If the map/struct were atomic, we'd break here, but we don't
// have a schema, so we can't tell.
m := w.value.AsMapUsing(w.allocator)
defer w.allocator.Free(m)
m.IterateUsing(w.allocator, func(k string, val value.Value) bool {
w2 := *w
w2.path = append(w.path, PathElement{FieldName: &k})
w2.value = val
w2.walk()
return true
})
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 (w *objectWalker) GuessBestListPathElement(index int, item value.Value) PathElement {
if !item.IsMap() {
// 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}
}
m := item.AsMapUsing(w.allocator)
defer w.allocator.Free(m)
var keys value.FieldList
for _, name := range AssociativeListCandidateFieldNames {
f, ok := m.Get(name)
if !ok {
continue
}
// only accept primitive/scalar types as keys.
if f.IsNull() || f.IsMap() || f.IsList() {
continue
}
keys = append(keys, value.Field{Name: name, Value: f})
}
if len(keys) > 0 {
keys.Sort()
return PathElement{Key: &keys}
}
return PathElement{Index: &index}
}
+144
View File
@@ -0,0 +1,144 @@
/*
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"
)
// APIVersion describes the version of an object or of a fieldset.
type APIVersion string
type VersionedSet interface {
Set() *Set
APIVersion() APIVersion
Applied() bool
}
// VersionedSet associates a version to a set.
type versionedSet struct {
set *Set
apiVersion APIVersion
applied bool
}
func NewVersionedSet(set *Set, apiVersion APIVersion, applied bool) VersionedSet {
return versionedSet{
set: set,
apiVersion: apiVersion,
applied: applied,
}
}
func (v versionedSet) Set() *Set {
return v.set
}
func (v versionedSet) APIVersion() APIVersion {
return v.apiVersion
}
func (v versionedSet) Applied() bool {
return v.applied
}
// ManagedFields is a map from manager to VersionedSet (what they own in
// what version).
type ManagedFields map[string]VersionedSet
// Equals returns true if the two managedfields are the same, false
// otherwise.
func (lhs ManagedFields) Equals(rhs ManagedFields) bool {
if len(lhs) != len(rhs) {
return false
}
for manager, left := range lhs {
right, ok := rhs[manager]
if !ok {
return false
}
if left.APIVersion() != right.APIVersion() || left.Applied() != right.Applied() {
return false
}
if !left.Set().Equals(right.Set()) {
return false
}
}
return true
}
// Copy the list, this is mostly a shallow copy.
func (lhs ManagedFields) Copy() ManagedFields {
copy := ManagedFields{}
for manager, set := range lhs {
copy[manager] = set
}
return copy
}
// 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.Set().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.Set().Difference(right.Set()).Union(right.Set().Difference(left.Set()))
if !newSet.Empty() {
diff[manager] = NewVersionedSet(newSet, right.APIVersion(), false)
}
}
for manager, set := range rhs {
if _, ok := lhs[manager]; ok {
// Already done
continue
}
if !set.Set().Empty() {
diff[manager] = set
}
}
return diff
}
func (lhs ManagedFields) String() string {
s := strings.Builder{}
for k, v := range lhs {
fmt.Fprintf(&s, "%s:\n", k)
fmt.Fprintf(&s, "- Applied: %v\n", v.Applied())
fmt.Fprintf(&s, "- APIVersion: %v\n", v.APIVersion())
fmt.Fprintf(&s, "- Set: %v\n", v.Set())
}
return s.String()
}
+118
View File
@@ -0,0 +1,118 @@
/*
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/v6/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, "")
}
// Equals returns true if the two paths are equivalent.
func (fp Path) Equals(fp2 Path) bool {
if len(fp) != len(fp2) {
return false
}
for i := range fp {
if !fp[i].Equals(fp2[i]) {
return false
}
}
return true
}
// Less provides a lexical order for Paths.
func (fp Path) Compare(rhs Path) int {
i := 0
for {
if i >= len(fp) && i >= len(rhs) {
// Paths are the same length and all items are equal.
return 0
}
if i >= len(fp) {
// LHS is shorter.
return -1
}
if i >= len(rhs) {
// RHS is shorter.
return 1
}
if c := fp[i].Compare(rhs[i]); c != 0 {
return c
}
// The items are equal; continue.
i++
}
}
func (fp Path) Copy() Path {
new := make(Path, len(fp))
copy(new, fp)
return new
}
// 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.FieldList:
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
}
+114
View File
@@ -0,0 +1,114 @@
/*
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 (
"sort"
"sigs.k8s.io/structured-merge-diff/v6/value"
)
// PathElementValueMap is a map from PathElement to value.Value.
//
// TODO(apelisse): We have multiple very similar implementation of this
// for PathElementSet and SetNodeMap, so we could probably share the
// code.
type PathElementValueMap struct {
valueMap PathElementMap
}
func MakePathElementValueMap(size int) PathElementValueMap {
return PathElementValueMap{
valueMap: MakePathElementMap(size),
}
}
type sortedPathElementValues []pathElementValue
// Implement the sort interface; this would permit bulk creation, which would
// be faster than doing it one at a time via Insert.
func (spev sortedPathElementValues) Len() int { return len(spev) }
func (spev sortedPathElementValues) Less(i, j int) bool {
return spev[i].PathElement.Less(spev[j].PathElement)
}
func (spev sortedPathElementValues) Swap(i, j int) { spev[i], spev[j] = spev[j], spev[i] }
// Insert adds the pathelement and associated value in the map.
// If insert is called twice with the same PathElement, the value is replaced.
func (s *PathElementValueMap) Insert(pe PathElement, v value.Value) {
s.valueMap.Insert(pe, v)
}
// Get retrieves the value associated with the given PathElement from the map.
// (nil, false) is returned if there is no such PathElement.
func (s *PathElementValueMap) Get(pe PathElement) (value.Value, bool) {
v, ok := s.valueMap.Get(pe)
if !ok {
return nil, false
}
return v.(value.Value), true
}
// PathElementValueMap is a map from PathElement to interface{}.
type PathElementMap struct {
members sortedPathElementValues
}
type pathElementValue struct {
PathElement PathElement
Value interface{}
}
func MakePathElementMap(size int) PathElementMap {
return PathElementMap{
members: make(sortedPathElementValues, 0, size),
}
}
// Insert adds the pathelement and associated value in the map.
// If insert is called twice with the same PathElement, the value is replaced.
func (s *PathElementMap) Insert(pe PathElement, v interface{}) {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].PathElement.Less(pe)
})
if loc == len(s.members) {
s.members = append(s.members, pathElementValue{pe, v})
return
}
if s.members[loc].PathElement.Equals(pe) {
s.members[loc].Value = v
return
}
s.members = append(s.members, pathElementValue{})
copy(s.members[loc+1:], s.members[loc:])
s.members[loc] = pathElementValue{pe, v}
}
// Get retrieves the value associated with the given PathElement from the map.
// (nil, false) is returned if there is no such PathElement.
func (s *PathElementMap) Get(pe PathElement) (interface{}, bool) {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].PathElement.Less(pe)
})
if loc == len(s.members) {
return nil, false
}
if s.members[loc].PathElement.Equals(pe) {
return s.members[loc].Value, true
}
return nil, false
}
+186
View File
@@ -0,0 +1,186 @@
/*
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 (
"errors"
"fmt"
"io"
"strconv"
"strings"
jsoniter "github.com/json-iterator/go"
"sigs.k8s.io/structured-merge-diff/v6/value"
)
var ErrUnknownPathElementType = errors.New("unknown path element type")
const (
// Field indicates that the content of this path element is a field's name
peField = "f"
// Value indicates that the content of this path element is a field's value
peValue = "v"
// Index indicates that the content of this path element is an index in an array
peIndex = "i"
// Key indicates that the content of this path element is a key value map
peKey = "k"
// Separator separates the type of a path element from the contents
peSeparator = ":"
)
var (
peFieldSepBytes = []byte(peField + peSeparator)
peValueSepBytes = []byte(peValue + peSeparator)
peIndexSepBytes = []byte(peIndex + peSeparator)
peKeySepBytes = []byte(peKey + peSeparator)
peSepBytes = []byte(peSeparator)
)
// readJSONIter reads a Value from a JSON iterator.
// DO NOT EXPORT
// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202
func readJSONIter(iter *jsoniter.Iterator) (value.Value, error) {
v := iter.Read()
if iter.Error != nil && iter.Error != io.EOF {
return nil, iter.Error
}
return value.NewValueInterface(v), nil
}
// writeJSONStream writes a value into a JSON stream.
// DO NOT EXPORT
// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202
func writeJSONStream(v value.Value, stream *jsoniter.Stream) {
stream.WriteVal(v.Unstructured())
}
// DeserializePathElement parses a serialized path element
func DeserializePathElement(s string) (PathElement, error) {
b := []byte(s)
if len(b) < 2 {
return PathElement{}, errors.New("key must be 2 characters long:")
}
typeSep, b := b[:2], b[2:]
if typeSep[1] != peSepBytes[0] {
return PathElement{}, fmt.Errorf("missing colon: %v", s)
}
switch typeSep[0] {
case peFieldSepBytes[0]:
// Slice s rather than convert b, to save on
// allocations.
str := s[2:]
return PathElement{
FieldName: &str,
}, nil
case peValueSepBytes[0]:
iter := readPool.BorrowIterator(b)
defer readPool.ReturnIterator(iter)
v, err := readJSONIter(iter)
if err != nil {
return PathElement{}, err
}
return PathElement{Value: &v}, nil
case peKeySepBytes[0]:
iter := readPool.BorrowIterator(b)
defer readPool.ReturnIterator(iter)
fields := value.FieldList{}
iter.ReadObjectCB(func(iter *jsoniter.Iterator, key string) bool {
v, err := readJSONIter(iter)
if err != nil {
iter.Error = err
return false
}
fields = append(fields, value.Field{Name: key, Value: v})
return true
})
fields.Sort()
return PathElement{Key: &fields}, iter.Error
case peIndexSepBytes[0]:
i, err := strconv.Atoi(s[2:])
if err != nil {
return PathElement{}, err
}
return PathElement{
Index: &i,
}, nil
default:
return PathElement{}, ErrUnknownPathElementType
}
}
var (
readPool = jsoniter.NewIterator(jsoniter.ConfigCompatibleWithStandardLibrary).Pool()
writePool = jsoniter.NewStream(jsoniter.ConfigCompatibleWithStandardLibrary, nil, 1024).Pool()
)
// SerializePathElement serializes a path element
func SerializePathElement(pe PathElement) (string, error) {
buf := strings.Builder{}
err := serializePathElementToWriter(&buf, pe)
return buf.String(), err
}
func serializePathElementToWriter(w io.Writer, pe PathElement) error {
stream := writePool.BorrowStream(w)
defer writePool.ReturnStream(stream)
switch {
case pe.FieldName != nil:
if _, err := stream.Write(peFieldSepBytes); err != nil {
return err
}
stream.WriteRaw(*pe.FieldName)
case pe.Key != nil:
if _, err := stream.Write(peKeySepBytes); err != nil {
return err
}
stream.WriteObjectStart()
for i, field := range *pe.Key {
if i > 0 {
stream.WriteMore()
}
stream.WriteObjectField(field.Name)
writeJSONStream(field.Value, stream)
}
stream.WriteObjectEnd()
case pe.Value != nil:
if _, err := stream.Write(peValueSepBytes); err != nil {
return err
}
writeJSONStream(*pe.Value, stream)
case pe.Index != nil:
if _, err := stream.Write(peIndexSepBytes); err != nil {
return err
}
stream.WriteInt(*pe.Index)
default:
return errors.New("invalid PathElement")
}
b := stream.Buffer()
err := stream.Flush()
// Help jsoniter manage its buffers--without this, the next
// use of the stream is likely to require an allocation. Look
// at the jsoniter stream code to understand why. They were probably
// optimizing for folks using the buffer directly.
stream.SetBuffer(b[:0])
return err
}
+238
View File
@@ -0,0 +1,238 @@
/*
Copyright 2019 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 (
"bytes"
"io"
"unsafe"
jsoniter "github.com/json-iterator/go"
)
func (s *Set) ToJSON() ([]byte, error) {
buf := bytes.Buffer{}
err := s.ToJSONStream(&buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (s *Set) ToJSONStream(w io.Writer) error {
stream := writePool.BorrowStream(w)
defer writePool.ReturnStream(stream)
var r reusableBuilder
stream.WriteObjectStart()
err := s.emitContentsV1(false, stream, &r)
if err != nil {
return err
}
stream.WriteObjectEnd()
return stream.Flush()
}
func manageMemory(stream *jsoniter.Stream) error {
// Help jsoniter manage its buffers--without this, it does a bunch of
// alloctaions that are not necessary. They were probably optimizing
// for folks using the buffer directly.
b := stream.Buffer()
if len(b) > 4096 || cap(b)-len(b) < 2048 {
if err := stream.Flush(); err != nil {
return err
}
stream.SetBuffer(b[:0])
}
return nil
}
type reusableBuilder struct {
bytes.Buffer
}
func (r *reusableBuilder) unsafeString() string {
b := r.Bytes()
return *(*string)(unsafe.Pointer(&b))
}
func (r *reusableBuilder) reset() *bytes.Buffer {
r.Reset()
return &r.Buffer
}
func (s *Set) emitContentsV1(includeSelf bool, stream *jsoniter.Stream, r *reusableBuilder) error {
mi, ci := 0, 0
first := true
preWrite := func() {
if first {
first = false
return
}
stream.WriteMore()
}
if includeSelf && !(len(s.Members.members) == 0 && len(s.Children.members) == 0) {
preWrite()
stream.WriteObjectField(".")
stream.WriteEmptyObject()
}
for mi < len(s.Members.members) && ci < len(s.Children.members) {
mpe := s.Members.members[mi]
cpe := s.Children.members[ci].pathElement
if c := mpe.Compare(cpe); c < 0 {
preWrite()
if err := serializePathElementToWriter(r.reset(), mpe); err != nil {
return err
}
stream.WriteObjectField(r.unsafeString())
stream.WriteEmptyObject()
mi++
} else if c > 0 {
preWrite()
if err := serializePathElementToWriter(r.reset(), cpe); err != nil {
return err
}
stream.WriteObjectField(r.unsafeString())
stream.WriteObjectStart()
if err := s.Children.members[ci].set.emitContentsV1(false, stream, r); err != nil {
return err
}
stream.WriteObjectEnd()
ci++
} else {
preWrite()
if err := serializePathElementToWriter(r.reset(), cpe); err != nil {
return err
}
stream.WriteObjectField(r.unsafeString())
stream.WriteObjectStart()
if err := s.Children.members[ci].set.emitContentsV1(true, stream, r); err != nil {
return err
}
stream.WriteObjectEnd()
mi++
ci++
}
}
for mi < len(s.Members.members) {
mpe := s.Members.members[mi]
preWrite()
if err := serializePathElementToWriter(r.reset(), mpe); err != nil {
return err
}
stream.WriteObjectField(r.unsafeString())
stream.WriteEmptyObject()
mi++
}
for ci < len(s.Children.members) {
cpe := s.Children.members[ci].pathElement
preWrite()
if err := serializePathElementToWriter(r.reset(), cpe); err != nil {
return err
}
stream.WriteObjectField(r.unsafeString())
stream.WriteObjectStart()
if err := s.Children.members[ci].set.emitContentsV1(false, stream, r); err != nil {
return err
}
stream.WriteObjectEnd()
ci++
}
return manageMemory(stream)
}
// FromJSON clears s and reads a JSON formatted set structure.
func (s *Set) FromJSON(r io.Reader) error {
// The iterator pool is completely useless for memory management, grrr.
iter := jsoniter.Parse(jsoniter.ConfigCompatibleWithStandardLibrary, r, 4096)
found, _ := readIterV1(iter)
if found == nil {
*s = Set{}
} else {
*s = *found
}
return iter.Error
}
// returns true if this subtree is also (or only) a member of parent; s is nil
// if there are no further children.
func readIterV1(iter *jsoniter.Iterator) (children *Set, isMember bool) {
iter.ReadMapCB(func(iter *jsoniter.Iterator, key string) bool {
if key == "." {
isMember = true
iter.Skip()
return true
}
pe, err := DeserializePathElement(key)
if err == ErrUnknownPathElementType {
// Ignore these-- a future version maybe knows what
// they are. We drop these completely rather than try
// to preserve things we don't understand.
iter.Skip()
return true
} else if err != nil {
iter.ReportError("parsing key as path element", err.Error())
iter.Skip()
return true
}
grandchildren, childIsMember := readIterV1(iter)
if childIsMember {
if children == nil {
children = &Set{}
}
m := &children.Members.members
// Since we expect that most of the time these will have been
// serialized in the right order, we just verify that and append.
appendOK := len(*m) == 0 || (*m)[len(*m)-1].Less(pe)
if appendOK {
*m = append(*m, pe)
} else {
children.Members.Insert(pe)
}
}
if grandchildren != nil {
if children == nil {
children = &Set{}
}
// Since we expect that most of the time these will have been
// serialized in the right order, we just verify that and append.
m := &children.Children.members
appendOK := len(*m) == 0 || (*m)[len(*m)-1].pathElement.Less(pe)
if appendOK {
*m = append(*m, setNode{pe, grandchildren})
} else {
*children.Children.Descend(pe) = *grandchildren
}
}
return true
})
if children == nil {
isMember = true
}
return children, isMember
}
+821
View File
@@ -0,0 +1,821 @@
/*
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"
"iter"
"sort"
"strings"
"sigs.k8s.io/structured-merge-diff/v6/value"
"sigs.k8s.io/structured-merge-diff/v6/schema"
)
// 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
}
// Copy returns a copy of the Set.
// This is not a full deep copy as any contained value.Value is not copied.
func (s *Set) Copy() *Set {
return &Set{
Members: s.Members.Copy(),
Children: s.Children.Copy(),
}
}
// 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
//
// 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),
}
}
// RecursiveDifference returns a Set containing elements which:
// * appear in s
// * do not appear in s2
//
// Compared to a regular difference,
// this removes every field **and its children** from s that is contained in s2.
//
// For example, with s containing `a.b.c` and s2 containing `a.b`,
// a RecursiveDifference will result in `a`, as the entire node `a.b` gets removed.
func (s *Set) RecursiveDifference(s2 *Set) *Set {
return &Set{
Members: *s.Members.Difference(&s2.Members),
Children: *s.Children.RecursiveDifference(s2),
}
}
// EnsureNamedFieldsAreMembers returns a Set that contains all the
// fields in s, as well as all the named fields that are typically not
// included. For example, a set made of "a.b.c" will end-up also owning
// "a" if it's a named fields but not "a.b" if it's a map.
func (s *Set) EnsureNamedFieldsAreMembers(sc *schema.Schema, tr schema.TypeRef) *Set {
members := PathElementSet{
members: make(sortedPathElements, 0, s.Members.Size()+len(s.Children.members)),
}
atom, _ := sc.Resolve(tr)
members.members = append(members.members, s.Members.members...)
for _, node := range s.Children.members {
// Only insert named fields.
if node.pathElement.FieldName != nil && atom.Map != nil {
if _, has := atom.Map.FindField(*node.pathElement.FieldName); has {
members.Insert(node.pathElement)
}
}
}
return &Set{
Members: members,
Children: *s.Children.EnsureNamedFieldsAreMembers(sc, tr),
}
}
// MakePrefixMatcherOrDie is the same as PrefixMatcher except it panics if parts can't be
// turned into a SetMatcher.
func MakePrefixMatcherOrDie(parts ...interface{}) *SetMatcher {
result, err := PrefixMatcher(parts...)
if err != nil {
panic(err)
}
return result
}
// PrefixMatcher creates a SetMatcher that matches all field paths prefixed by the given list of matcher path parts.
// The matcher parts may any of:
//
// - PathElementMatcher - for wildcards, `MatchAnyPathElement()` can be used as well.
// - PathElement - for any path element
// - value.FieldList - for listMap keys
// - value.Value - for scalar list elements
// - string - For field names
// - int - for array indices
func PrefixMatcher(parts ...interface{}) (*SetMatcher, error) {
current := MatchAnySet() // match all field path suffixes
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
var pattern PathElementMatcher
switch t := part.(type) {
case PathElementMatcher:
// any path matcher, including wildcard
pattern = t
case PathElement:
// any path element
pattern = PathElementMatcher{PathElement: t}
case *value.FieldList:
// a listMap key
if len(*t) == 0 {
return nil, fmt.Errorf("associative list key type path elements must have at least one key (got zero)")
}
pattern = PathElementMatcher{PathElement: PathElement{Key: t}}
case value.Value:
// a scalar or set-type list element
pattern = PathElementMatcher{PathElement: PathElement{Value: &t}}
case string:
// a plain field name
pattern = PathElementMatcher{PathElement: PathElement{FieldName: &t}}
case int:
// a plain list index
pattern = PathElementMatcher{PathElement: PathElement{Index: &t}}
default:
return nil, fmt.Errorf("unexpected type %T", t)
}
current = &SetMatcher{
members: []*SetMemberMatcher{{
Path: pattern,
Child: current,
}},
}
}
return current, nil
}
// MatchAnyPathElement returns a PathElementMatcher that matches any path element.
func MatchAnyPathElement() PathElementMatcher {
return PathElementMatcher{Wildcard: true}
}
// MatchAnySet returns a SetMatcher that matches any set.
func MatchAnySet() *SetMatcher {
return &SetMatcher{wildcard: true}
}
// NewSetMatcher returns a new SetMatcher.
// Wildcard members take precedent over non-wildcard members;
// all non-wildcard members are ignored if there is a wildcard members.
func NewSetMatcher(wildcard bool, members ...*SetMemberMatcher) *SetMatcher {
sort.Sort(sortedMemberMatcher(members))
return &SetMatcher{wildcard: wildcard, members: members}
}
// SetMatcher defines a matcher that matches fields in a Set.
// SetMatcher is structured much like a Set but with wildcard support.
type SetMatcher struct {
// wildcard indicates that all members and children are included in the match.
// If set, the members field is ignored.
wildcard bool
// members provides patterns to match the members of a Set.
// Wildcard members are sorted before non-wildcards and take precedent over
// non-wildcard members.
members sortedMemberMatcher
}
type sortedMemberMatcher []*SetMemberMatcher
func (s sortedMemberMatcher) Len() int { return len(s) }
func (s sortedMemberMatcher) Less(i, j int) bool { return s[i].Path.Less(s[j].Path) }
func (s sortedMemberMatcher) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s sortedMemberMatcher) Find(p PathElementMatcher) (location int, ok bool) {
return sort.Find(len(s), func(i int) int {
return s[i].Path.Compare(p)
})
}
// Merge merges s and s2 and returns a SetMatcher that matches all field paths matched by either s or s2.
// During the merge, members of s and s2 with the same PathElementMatcher merged into a single member
// with the children of each merged by calling this function recursively.
func (s *SetMatcher) Merge(s2 *SetMatcher) *SetMatcher {
if s.wildcard || s2.wildcard {
return NewSetMatcher(true)
}
merged := make(sortedMemberMatcher, len(s.members), len(s.members)+len(s2.members))
copy(merged, s.members)
for _, m := range s2.members {
if i, ok := s.members.Find(m.Path); ok {
// since merged is a shallow copy, do not modify elements in place
merged[i] = &SetMemberMatcher{
Path: merged[i].Path,
Child: merged[i].Child.Merge(m.Child),
}
} else {
merged = append(merged, m)
}
}
return NewSetMatcher(false, merged...) // sort happens here
}
// SetMemberMatcher defines a matcher that matches the members of a Set.
// SetMemberMatcher is structured much like the elements of a SetNodeMap, but
// with wildcard support.
type SetMemberMatcher struct {
// Path provides a matcher to match members of a Set.
// If Path is a wildcard, all members of a Set are included in the match.
// Otherwise, if any Path is Equal to a member of a Set, that member is
// included in the match and the children of that member are matched
// against the Child matcher.
Path PathElementMatcher
// Child provides a matcher to use for the children of matched members of a Set.
Child *SetMatcher
}
// PathElementMatcher defined a path matcher for a PathElement.
type PathElementMatcher struct {
// Wildcard indicates that all PathElements are matched by this matcher.
// If set, PathElement is ignored.
Wildcard bool
// PathElement indicates that a PathElement is matched if it is Equal
// to this PathElement.
PathElement
}
func (p PathElementMatcher) Equals(p2 PathElementMatcher) bool {
return p.Wildcard != p2.Wildcard && p.PathElement.Equals(p2.PathElement)
}
func (p PathElementMatcher) Less(p2 PathElementMatcher) bool {
if p.Wildcard && !p2.Wildcard {
return true
} else if p2.Wildcard {
return false
}
return p.PathElement.Less(p2.PathElement)
}
func (p PathElementMatcher) Compare(p2 PathElementMatcher) int {
if p.Wildcard && !p2.Wildcard {
return -1
} else if p2.Wildcard {
return 1
}
return p.PathElement.Compare(p2.PathElement)
}
// FilterIncludeMatches returns a Set with only the field paths that match.
func (s *Set) FilterIncludeMatches(pattern *SetMatcher) *Set {
if pattern.wildcard {
return s
}
members := PathElementSet{}
for _, m := range s.Members.members {
for _, pm := range pattern.members {
if pm.Path.Wildcard || pm.Path.PathElement.Equals(m) {
members.Insert(m)
break
}
}
}
return &Set{
Members: members,
Children: *s.Children.FilterIncludeMatches(pattern),
}
}
// 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)
}
// All iterates over each Path in the set (preorder DFS).
func (s *Set) All() iter.Seq[Path] {
return func(yield func(Path) bool) {
s.Iterate(func(p Path) {
yield(p)
})
}
}
func (s *Set) iteratePrefix(prefix Path, f func(Path)) {
s.Members.Iterate(func(pe PathElement) { f(append(prefix, pe)) })
s.Children.iteratePrefix(prefix, f)
}
// WithPrefix returns the subset of paths which begin with the given prefix,
// with the prefix not included.
func (s *Set) WithPrefix(pe PathElement) *Set {
subset, ok := s.Children.Get(pe)
if !ok {
return NewSet()
}
return subset
}
// Leaves returns a set containing only the leaf paths
// of a set.
func (s *Set) Leaves() *Set {
leaves := PathElementSet{}
im := 0
ic := 0
// any members that are not also children are leaves
outer:
for im < len(s.Members.members) {
member := s.Members.members[im]
for ic < len(s.Children.members) {
d := member.Compare(s.Children.members[ic].pathElement)
if d == 0 {
ic++
im++
continue outer
} else if d < 0 {
break
} else /* if d > 0 */ {
ic++
}
}
leaves.members = append(leaves.members, member)
im++
}
return &Set{
Members: leaves,
Children: *s.Children.Leaves(),
}
}
// 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 sortedSetNode
}
type sortedSetNode []setNode
// Implement the sort interface; this would permit bulk creation, which would
// be faster than doing it one at a time via Insert.
func (s sortedSetNode) Len() int { return len(s) }
func (s sortedSetNode) Less(i, j int) bool { return s[i].pathElement.Less(s[j].pathElement) }
func (s sortedSetNode) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// Copy returns a copy of the SetNodeMap.
// This is not a full deep copy as any contained value.Value is not copied.
func (s *SetNodeMap) Copy() SetNodeMap {
out := make(sortedSetNode, len(s.members))
for i, v := range s.members {
out[i] = setNode{pathElement: v.pathElement.Copy(), set: v.set.Copy()}
}
return SetNodeMap{members: out}
}
// Descend adds pe to the set if necessary, returning the associated subset.
func (s *SetNodeMap) Descend(pe PathElement) *Set {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].pathElement.Less(pe)
})
if loc == len(s.members) {
s.members = append(s.members, setNode{pathElement: pe, set: &Set{}})
return s.members[loc].set
}
if s.members[loc].pathElement.Equals(pe) {
return s.members[loc].set
}
s.members = append(s.members, setNode{})
copy(s.members[loc+1:], s.members[loc:])
s.members[loc] = setNode{pathElement: pe, set: &Set{}}
return s.members[loc].set
}
// 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) {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].pathElement.Less(pe)
})
if loc == len(s.members) {
return nil, false
}
if s.members[loc].pathElement.Equals(pe) {
return s.members[loc].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 i := range s.members {
if !s.members[i].pathElement.Equals(s2.members[i].pathElement) {
return false
}
if !s.members[i].set.Equals(s2.members[i].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{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.members) {
if s.members[i].pathElement.Less(s2.members[j].pathElement) {
out.members = append(out.members, s.members[i])
i++
} else {
if !s2.members[j].pathElement.Less(s.members[i].pathElement) {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: s.members[i].set.Union(s2.members[j].set)})
i++
} else {
out.members = append(out.members, s2.members[j])
}
j++
}
}
if i < len(s.members) {
out.members = append(out.members, s.members[i:]...)
}
if j < len(s2.members) {
out.members = append(out.members, s2.members[j:]...)
}
return out
}
// Intersection returns a SetNodeMap with members that appear in both s and s2.
func (s *SetNodeMap) Intersection(s2 *SetNodeMap) *SetNodeMap {
out := &SetNodeMap{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.members) {
if s.members[i].pathElement.Less(s2.members[j].pathElement) {
i++
} else {
if !s2.members[j].pathElement.Less(s.members[i].pathElement) {
res := s.members[i].set.Intersection(s2.members[j].set)
if !res.Empty() {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: res})
}
i++
}
j++
}
}
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{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.Children.members) {
if s.members[i].pathElement.Less(s2.Children.members[j].pathElement) {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: s.members[i].set})
i++
} else {
if !s2.Children.members[j].pathElement.Less(s.members[i].pathElement) {
diff := s.members[i].set.Difference(s2.Children.members[j].set)
// We aren't permitted to add nodes with no elements.
if !diff.Empty() {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: diff})
}
i++
}
j++
}
}
if i < len(s.members) {
out.members = append(out.members, s.members[i:]...)
}
return out
}
// RecursiveDifference returns a SetNodeMap with members that appear in s but not in s2.
//
// Compared to a regular difference,
// this removes every field **and its children** from s that is contained in s2.
//
// For example, with s containing `a.b.c` and s2 containing `a.b`,
// a RecursiveDifference will result in `a`, as the entire node `a.b` gets removed.
func (s *SetNodeMap) RecursiveDifference(s2 *Set) *SetNodeMap {
out := &SetNodeMap{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.Children.members) {
if s.members[i].pathElement.Less(s2.Children.members[j].pathElement) {
if !s2.Members.Has(s.members[i].pathElement) {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: s.members[i].set})
}
i++
} else {
if !s2.Children.members[j].pathElement.Less(s.members[i].pathElement) {
if !s2.Members.Has(s.members[i].pathElement) {
diff := s.members[i].set.RecursiveDifference(s2.Children.members[j].set)
if !diff.Empty() {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: diff})
}
}
i++
}
j++
}
}
if i < len(s.members) {
for _, c := range s.members[i:] {
if !s2.Members.Has(c.pathElement) {
out.members = append(out.members, c)
}
}
}
return out
}
// EnsureNamedFieldsAreMembers returns a set that contains all the named fields along with the leaves.
func (s *SetNodeMap) EnsureNamedFieldsAreMembers(sc *schema.Schema, tr schema.TypeRef) *SetNodeMap {
out := make(sortedSetNode, 0, s.Size())
atom, _ := sc.Resolve(tr)
for _, member := range s.members {
tr := schema.TypeRef{}
if member.pathElement.FieldName != nil && atom.Map != nil {
tr = atom.Map.ElementType
if sf, ok := atom.Map.FindField(*member.pathElement.FieldName); ok {
tr = sf.Type
}
} else if member.pathElement.Key != nil && atom.List != nil {
tr = atom.List.ElementType
}
out = append(out, setNode{
pathElement: member.pathElement,
set: member.set.EnsureNamedFieldsAreMembers(sc, tr),
})
}
return &SetNodeMap{
members: out,
}
}
// FilterIncludeMatches returns a SetNodeMap with only the field paths that match the matcher.
func (s *SetNodeMap) FilterIncludeMatches(pattern *SetMatcher) *SetNodeMap {
if pattern.wildcard {
return s
}
var out sortedSetNode
for _, member := range s.members {
for _, c := range pattern.members {
if c.Path.Wildcard || c.Path.PathElement.Equals(member.pathElement) {
childSet := member.set.FilterIncludeMatches(c.Child)
if childSet.Size() > 0 {
out = append(out, setNode{
pathElement: member.pathElement,
set: childSet,
})
}
break
}
}
}
return &SetNodeMap{
members: 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)
}
}
// All iterates over each PathElement in the set.
func (s *SetNodeMap) All() iter.Seq[PathElement] {
return func(yield func(PathElement) bool) {
s.Iterate(func(pe PathElement) {
yield(pe)
})
}
}
func (s *SetNodeMap) iteratePrefix(prefix Path, f func(Path)) {
for _, n := range s.members {
pe := n.pathElement
n.set.iteratePrefix(append(prefix, pe), f)
}
}
// Leaves returns a SetNodeMap containing
// only setNodes with leaf PathElements.
func (s *SetNodeMap) Leaves() *SetNodeMap {
out := &SetNodeMap{}
out.members = make(sortedSetNode, len(s.members))
for i, n := range s.members {
out.members[i] = setNode{
pathElement: n.pathElement,
set: n.set.Leaves(),
}
}
return out
}
// Filter defines an interface for excluding field paths from a set.
// NewExcludeSetFilter can be used to create a filter that removes
// specific field paths and all of their children.
// NewIncludeMatcherFilter can be used to create a filter that removes all fields except
// the fields that match a field path matcher. PrefixMatcher and MakePrefixMatcherOrDie
// can be used to define field path patterns.
type Filter interface {
// Filter returns a filtered copy of the set.
Filter(*Set) *Set
}
// NewExcludeSetFilter returns a filter that removes field paths in the exclude set.
func NewExcludeSetFilter(exclude *Set) Filter {
return excludeFilter{exclude}
}
// NewExcludeFilterSetMap converts a map of APIVersion to exclude set to a map of APIVersion to exclude filters.
func NewExcludeFilterSetMap(resetFields map[APIVersion]*Set) map[APIVersion]Filter {
result := make(map[APIVersion]Filter)
for k, v := range resetFields {
result[k] = excludeFilter{v}
}
return result
}
type excludeFilter struct {
excludeSet *Set
}
func (t excludeFilter) Filter(set *Set) *Set {
return set.RecursiveDifference(t.excludeSet)
}
// NewIncludeMatcherFilter returns a filter that only includes field paths that match.
// If no matchers are provided, the filter includes all field paths.
// PrefixMatcher and MakePrefixMatcherOrDie can help create basic matcher.
func NewIncludeMatcherFilter(matchers ...*SetMatcher) Filter {
if len(matchers) == 0 {
return includeMatcherFilter{&SetMatcher{wildcard: true}}
}
matcher := matchers[0]
for i := 1; i < len(matchers); i++ {
matcher = matcher.Merge(matchers[i])
}
return includeMatcherFilter{matcher}
}
type includeMatcherFilter struct {
matcher *SetMatcher
}
func (pf includeMatcherFilter) Filter(set *Set) *Set {
return set.FilterIncludeMatches(pf.matcher)
}
+121
View File
@@ -0,0 +1,121 @@
/*
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/v6/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)
}
// Equals returns true if c == c2
func (c Conflict) Equals(c2 Conflict) bool {
if c.Manager != c2.Manager {
return false
}
return c.Path.Equals(c2.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")
}
// Equals returns true if the lists of conflicts are the same.
func (c Conflicts) Equals(c2 Conflicts) bool {
if len(c) != len(c2) {
return false
}
for i := range c {
if !c[i].Equals(c2[i]) {
return false
}
}
return true
}
// ToSet aggregates conflicts for all managers into a single Set.
func (c Conflicts) ToSet() *fieldpath.Set {
set := fieldpath.NewSet()
for _, conflict := range []Conflict(c) {
set.Insert(conflict.Path)
}
return set
}
// ConflictsFromManagers creates a list of conflicts given Managers sets.
func ConflictsFromManagers(sets fieldpath.ManagedFields) Conflicts {
conflicts := []Conflict{}
for manager, set := range sets {
set.Set().Iterate(func(p fieldpath.Path) {
conflicts = append(conflicts, Conflict{
Manager: manager,
Path: p.Copy(),
})
})
}
return conflicts
}
+395
View File
@@ -0,0 +1,395 @@
/*
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/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/typed"
"sigs.k8s.io/structured-merge-diff/v6/value"
)
// 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)
IsMissingVersionError(error) bool
}
// UpdateBuilder allows you to create a new Updater by exposing all of
// the options and setting them once.
type UpdaterBuilder struct {
Converter Converter
IgnoreFilter map[fieldpath.APIVersion]fieldpath.Filter
// IgnoredFields provides a set of fields to ignore for each
IgnoredFields map[fieldpath.APIVersion]*fieldpath.Set
// Stop comparing the new object with old object after applying.
// This was initially used to avoid spurious etcd update, but
// since that's vastly inefficient, we've come-up with a better
// way of doing that. Create this flag to stop it.
// Comparing has become more expensive too now that we're not using
// `Compare` but `value.Equals` so this gives an option to avoid it.
ReturnInputOnNoop bool
}
func (u *UpdaterBuilder) BuildUpdater() *Updater {
return &Updater{
Converter: u.Converter,
IgnoreFilter: u.IgnoreFilter,
IgnoredFields: u.IgnoredFields,
returnInputOnNoop: u.ReturnInputOnNoop,
}
}
// Updater is the object used to compute updated FieldSets and also
// merge the object on Apply.
type Updater struct {
// Deprecated: This will eventually become private.
Converter Converter
// Deprecated: This will eventually become private.
IgnoredFields map[fieldpath.APIVersion]*fieldpath.Set
// Deprecated: This will eventually become private.
IgnoreFilter map[fieldpath.APIVersion]fieldpath.Filter
returnInputOnNoop bool
}
func (s *Updater) update(oldObject, newObject *typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, workflow string, force bool) (fieldpath.ManagedFields, *typed.Comparison, error) {
conflicts := fieldpath.ManagedFields{}
removed := fieldpath.ManagedFields{}
compare, err := oldObject.Compare(newObject)
if err != nil {
return nil, nil, fmt.Errorf("failed to compare objects: %v", err)
}
var versions map[fieldpath.APIVersion]*typed.Comparison
if s.IgnoredFields != nil && s.IgnoreFilter != nil {
return nil, nil, fmt.Errorf("IgnoreFilter and IgnoreFilter may not both be set")
}
if s.IgnoredFields != nil {
versions = map[fieldpath.APIVersion]*typed.Comparison{
version: compare.ExcludeFields(s.IgnoredFields[version]),
}
} else {
versions = map[fieldpath.APIVersion]*typed.Comparison{
version: compare.FilterFields(s.IgnoreFilter[version]),
}
}
for manager, managerSet := range managers {
if manager == workflow {
continue
}
compare, ok := versions[managerSet.APIVersion()]
if !ok {
var err error
versionedOldObject, err := s.Converter.Convert(oldObject, managerSet.APIVersion())
if err != nil {
if s.Converter.IsMissingVersionError(err) {
delete(managers, manager)
continue
}
return nil, nil, fmt.Errorf("failed to convert old object: %v", err)
}
versionedNewObject, err := s.Converter.Convert(newObject, managerSet.APIVersion())
if err != nil {
if s.Converter.IsMissingVersionError(err) {
delete(managers, manager)
continue
}
return nil, nil, fmt.Errorf("failed to convert new object: %v", err)
}
compare, err = versionedOldObject.Compare(versionedNewObject)
if err != nil {
return nil, nil, fmt.Errorf("failed to compare objects: %v", err)
}
if s.IgnoredFields != nil {
versions[managerSet.APIVersion()] = compare.ExcludeFields(s.IgnoredFields[managerSet.APIVersion()])
} else {
versions[managerSet.APIVersion()] = compare.FilterFields(s.IgnoreFilter[managerSet.APIVersion()])
}
}
conflictSet := managerSet.Set().Intersection(compare.Modified.Union(compare.Added))
if !conflictSet.Empty() {
conflicts[manager] = fieldpath.NewVersionedSet(conflictSet, managerSet.APIVersion(), false)
}
if !compare.Removed.Empty() {
removed[manager] = fieldpath.NewVersionedSet(compare.Removed, managerSet.APIVersion(), false)
}
}
if !force && len(conflicts) != 0 {
return nil, nil, ConflictsFromManagers(conflicts)
}
for manager, conflictSet := range conflicts {
managers[manager] = fieldpath.NewVersionedSet(managers[manager].Set().Difference(conflictSet.Set()), managers[manager].APIVersion(), managers[manager].Applied())
}
for manager, removedSet := range removed {
managers[manager] = fieldpath.NewVersionedSet(managers[manager].Set().Difference(removedSet.Set()), managers[manager].APIVersion(), managers[manager].Applied())
}
for manager := range managers {
if managers[manager].Set().Empty() {
delete(managers, manager)
}
}
return managers, compare, 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) (*typed.TypedValue, fieldpath.ManagedFields, error) {
var err error
managers, err = s.reconcileManagedFieldsWithSchemaChanges(liveObject, managers)
if err != nil {
return nil, fieldpath.ManagedFields{}, err
}
managers, compare, err := s.update(liveObject, newObject, version, managers, manager, true)
if err != nil {
return nil, fieldpath.ManagedFields{}, err
}
if _, ok := managers[manager]; !ok {
managers[manager] = fieldpath.NewVersionedSet(fieldpath.NewSet(), version, false)
}
set := managers[manager].Set().Difference(compare.Removed).Union(compare.Modified).Union(compare.Added)
if s.IgnoredFields != nil && s.IgnoreFilter != nil {
return nil, nil, fmt.Errorf("IgnoreFilter and IgnoreFilter may not both be set")
}
var ignoreFilter fieldpath.Filter
if s.IgnoredFields != nil {
ignoreFilter = fieldpath.NewExcludeSetFilter(s.IgnoredFields[version])
} else {
ignoreFilter = s.IgnoreFilter[version]
}
if ignoreFilter != nil {
set = ignoreFilter.Filter(set)
}
managers[manager] = fieldpath.NewVersionedSet(
set,
version,
false,
)
if managers[manager].Set().Empty() {
delete(managers, manager)
}
return newObject, 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) {
var err error
managers, err = s.reconcileManagedFieldsWithSchemaChanges(liveObject, managers)
if err != nil {
return nil, fieldpath.ManagedFields{}, err
}
newObject, err := liveObject.Merge(configObject)
if err != nil {
return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to merge config: %v", err)
}
lastSet := managers[manager]
set, err := configObject.ToFieldSet()
if err != nil {
return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to get field set: %v", err)
}
if s.IgnoredFields != nil && s.IgnoreFilter != nil {
return nil, nil, fmt.Errorf("IgnoreFilter and IgnoreFilter may not both be set")
}
var ignoreFilter fieldpath.Filter
if s.IgnoredFields != nil {
ignoreFilter = fieldpath.NewExcludeSetFilter(s.IgnoredFields[version])
} else {
ignoreFilter = s.IgnoreFilter[version]
}
if ignoreFilter != nil {
set = ignoreFilter.Filter(set)
}
managers[manager] = fieldpath.NewVersionedSet(set, version, true)
newObject, err = s.prune(newObject, managers, manager, lastSet)
if err != nil {
return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to prune fields: %v", err)
}
managers, _, err = s.update(liveObject, newObject, version, managers, manager, force)
if err != nil {
return nil, fieldpath.ManagedFields{}, err
}
if !s.returnInputOnNoop && value.EqualsUsing(value.NewFreelistAllocator(), liveObject.AsValue(), newObject.AsValue()) {
newObject = nil
}
return newObject, managers, nil
}
// prune will remove a field, list or map item, iff:
// * applyingManager applied it last time
// * applyingManager didn't apply it this time
// * no other applier claims to manage it
func (s *Updater) prune(merged *typed.TypedValue, managers fieldpath.ManagedFields, applyingManager string, lastSet fieldpath.VersionedSet) (*typed.TypedValue, error) {
if lastSet == nil || lastSet.Set().Empty() {
return merged, nil
}
version := lastSet.APIVersion()
convertedMerged, err := s.Converter.Convert(merged, version)
if err != nil {
if s.Converter.IsMissingVersionError(err) {
return merged, nil
}
return nil, fmt.Errorf("failed to convert merged object to last applied version: %v", err)
}
sc, tr := convertedMerged.Schema(), convertedMerged.TypeRef()
pruned := convertedMerged.RemoveItems(lastSet.Set().EnsureNamedFieldsAreMembers(sc, tr))
pruned, err = s.addBackOwnedItems(convertedMerged, pruned, version, managers, applyingManager)
if err != nil {
return nil, fmt.Errorf("failed add back owned items: %v", err)
}
pruned, err = s.addBackDanglingItems(convertedMerged, pruned, lastSet)
if err != nil {
return nil, fmt.Errorf("failed add back dangling items: %v", err)
}
return s.Converter.Convert(pruned, managers[applyingManager].APIVersion())
}
// addBackOwnedItems adds back any fields, list and map items that were removed by prune,
// but other appliers or updaters (or the current applier's new config) claim to own.
func (s *Updater) addBackOwnedItems(merged, pruned *typed.TypedValue, prunedVersion fieldpath.APIVersion, managedFields fieldpath.ManagedFields, applyingManager string) (*typed.TypedValue, error) {
var err error
managedAtVersion := map[fieldpath.APIVersion]*fieldpath.Set{}
for _, managerSet := range managedFields {
if _, ok := managedAtVersion[managerSet.APIVersion()]; !ok {
managedAtVersion[managerSet.APIVersion()] = fieldpath.NewSet()
}
managedAtVersion[managerSet.APIVersion()] = managedAtVersion[managerSet.APIVersion()].Union(managerSet.Set())
}
// Add back owned items at pruned version first to avoid conversion failure
// caused by pruned fields which are required for conversion.
if managed, ok := managedAtVersion[prunedVersion]; ok {
merged, pruned, err = s.addBackOwnedItemsForVersion(merged, pruned, prunedVersion, managed)
if err != nil {
return nil, err
}
delete(managedAtVersion, prunedVersion)
}
for version, managed := range managedAtVersion {
merged, pruned, err = s.addBackOwnedItemsForVersion(merged, pruned, version, managed)
if err != nil {
return nil, err
}
}
return pruned, nil
}
// addBackOwnedItemsForVersion adds back any fields, list and map items that were removed by prune with specific managed field path at a version.
// It is an extracted sub-function from addBackOwnedItems for code reuse.
func (s *Updater) addBackOwnedItemsForVersion(merged, pruned *typed.TypedValue, version fieldpath.APIVersion, managed *fieldpath.Set) (*typed.TypedValue, *typed.TypedValue, error) {
var err error
merged, err = s.Converter.Convert(merged, version)
if err != nil {
if s.Converter.IsMissingVersionError(err) {
return merged, pruned, nil
}
return nil, nil, fmt.Errorf("failed to convert merged object at version %v: %v", version, err)
}
pruned, err = s.Converter.Convert(pruned, version)
if err != nil {
if s.Converter.IsMissingVersionError(err) {
return merged, pruned, nil
}
return nil, nil, fmt.Errorf("failed to convert pruned object at version %v: %v", version, err)
}
mergedSet, err := merged.ToFieldSet()
if err != nil {
return nil, nil, fmt.Errorf("failed to create field set from merged object at version %v: %v", version, err)
}
prunedSet, err := pruned.ToFieldSet()
if err != nil {
return nil, nil, fmt.Errorf("failed to create field set from pruned object at version %v: %v", version, err)
}
sc, tr := merged.Schema(), merged.TypeRef()
pruned = merged.RemoveItems(mergedSet.EnsureNamedFieldsAreMembers(sc, tr).Difference(prunedSet.EnsureNamedFieldsAreMembers(sc, tr).Union(managed.EnsureNamedFieldsAreMembers(sc, tr))))
return merged, pruned, nil
}
// addBackDanglingItems makes sure that the fields list and map items removed by prune were
// previously owned by the currently applying manager. This will add back fields list and map items
// that are unowned or that are owned by Updaters and shouldn't be removed.
func (s *Updater) addBackDanglingItems(merged, pruned *typed.TypedValue, lastSet fieldpath.VersionedSet) (*typed.TypedValue, error) {
convertedPruned, err := s.Converter.Convert(pruned, lastSet.APIVersion())
if err != nil {
if s.Converter.IsMissingVersionError(err) {
return merged, nil
}
return nil, fmt.Errorf("failed to convert pruned object to last applied version: %v", err)
}
prunedSet, err := convertedPruned.ToFieldSet()
if err != nil {
return nil, fmt.Errorf("failed to create field set from pruned object in last applied version: %v", err)
}
mergedSet, err := merged.ToFieldSet()
if err != nil {
return nil, fmt.Errorf("failed to create field set from merged object in last applied version: %v", err)
}
sc, tr := merged.Schema(), merged.TypeRef()
prunedSet = prunedSet.EnsureNamedFieldsAreMembers(sc, tr)
mergedSet = mergedSet.EnsureNamedFieldsAreMembers(sc, tr)
last := lastSet.Set().EnsureNamedFieldsAreMembers(sc, tr)
return merged.RemoveItems(mergedSet.Difference(prunedSet).Intersection(last)), nil
}
// reconcileManagedFieldsWithSchemaChanges reconciles the managed fields with any changes to the
// object's schema since the managed fields were written.
//
// Supports:
// - changing types from atomic to granular
// - changing types from granular to atomic
func (s *Updater) reconcileManagedFieldsWithSchemaChanges(liveObject *typed.TypedValue, managers fieldpath.ManagedFields) (fieldpath.ManagedFields, error) {
result := fieldpath.ManagedFields{}
for manager, versionedSet := range managers {
tv, err := s.Converter.Convert(liveObject, versionedSet.APIVersion())
if s.Converter.IsMissingVersionError(err) { // okay to skip, obsolete versions will be deleted automatically anyway
continue
}
if err != nil {
return nil, err
}
reconciled, err := typed.ReconcileFieldSetWithSchema(versionedSet.Set(), tv)
if err != nil {
return nil, err
}
if reconciled != nil {
result[manager] = fieldpath.NewVersionedSet(reconciled, versionedSet.APIVersion(), versionedSet.Applied())
} else {
result[manager] = versionedSet
}
}
return result, nil
}
+28
View File
@@ -0,0 +1,28 @@
/*
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
+382
View File
@@ -0,0 +1,382 @@
/*
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
import (
"sync"
"sync/atomic"
)
// Schema is a list of named types.
//
// Schema types are indexed in a map before the first search so this type
// should be considered immutable.
type Schema struct {
Types []TypeDef `yaml:"types,omitempty"`
once sync.Once
m atomic.Pointer[map[string]TypeDef]
lock sync.Mutex
// Cached results of resolving type references to atoms. Only stores
// type references which require fields of Atom to be overriden.
resolvedTypes map[TypeRef]Atom
}
// 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"`
// If this reference refers to a map-type or list-type, this field overrides
// the `ElementRelationship` of the referred type when resolved.
// If this field is nil, then it has no effect.
// See `Map` and `List` for more information about `ElementRelationship`
ElementRelationship *ElementRelationship `yaml:"elementRelationship,omitempty"`
}
// Atom represents the smallest possible pieces of the type system.
// Each set field in the Atom represents a possible type for the object.
// If none of the fields are set, any object will fail validation against the atom.
type Atom struct {
*Scalar `yaml:"scalar,omitempty"`
*List `yaml:"list,omitempty"`
*Map `yaml:"map,omitempty"`
}
// Scalar (AKA "primitive") represents a type which has a single value which is
// either numeric, string, or boolean, or untyped for any of them.
//
// TODO: split numeric into float/int? Something even more fine-grained?
type Scalar string
const (
Numeric = Scalar("numeric")
String = Scalar("string")
Boolean = Scalar("boolean")
Untyped = Scalar("untyped")
)
// ElementRelationship is an enum of the different possible relationships
// between the elements of container types (maps, lists).
type ElementRelationship string
const (
// Associative only applies to lists (see the documentation there).
Associative = ElementRelationship("associative")
// Atomic makes container types (lists, maps) behave
// as scalars / leaf fields
Atomic = ElementRelationship("atomic")
// Separable means the items of the container type have no particular
// relationship (default behavior for maps).
Separable = ElementRelationship("separable")
)
// 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.
//
// 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).
//
// Maps may also represent a type which is composed of a number of different fields.
// Each field has a name and a type.
//
// Fields are indexed in a map before the first search so this type
// should be considered immutable.
type Map 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"`
// A Union is a grouping of fields with special rules. It 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.
// One can have multiple unions in the same struct, but the fields can't
// overlap between unions.
Unions []Union `yaml:"unions,omitempty"`
// ElementType is the type of the structs's unknown fields.
ElementType TypeRef `yaml:"elementType,omitempty"`
// ElementRelationship states the relationship between the map'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 maps is `separable`; it's permitted to
// leave this unset to get the default behavior.
ElementRelationship ElementRelationship `yaml:"elementRelationship,omitempty"`
once sync.Once
m atomic.Pointer[map[string]StructField]
}
// FindField is a convenience function that returns the referenced StructField,
// if it exists, or (nil, false) if it doesn't.
func (m *Map) FindField(name string) (StructField, bool) {
m.once.Do(func() {
mm := make(map[string]StructField, len(m.Fields))
for _, field := range m.Fields {
mm[field.Name] = field
}
m.m.Store(&mm)
})
sf, ok := (*m.m.Load())[name]
return sf, ok
}
// CopyInto clones this instance of Map into dst
//
// If dst is nil this method does nothing.
// If dst is already initialized, overwrites it with this instance.
// Warning: Not thread safe. Only use dst after this function returns.
func (m *Map) CopyInto(dst *Map) {
if dst == nil {
return
}
// Map type is considered immutable so sharing references
dst.Fields = m.Fields
dst.ElementType = m.ElementType
dst.Unions = m.Unions
dst.ElementRelationship = m.ElementRelationship
mm := m.m.Load()
if mm != nil {
// If cache is non-nil then the once token had been consumed.
// Must reset token and use it again to ensure same semantics.
dst.once = sync.Once{}
dst.once.Do(func() {
dst.m.Store(mm)
})
}
}
// UnionFields are mapping between the fields that are part of the union and
// their discriminated value. The discriminated value has to be set, and
// should not conflict with other discriminated value in the list.
type UnionField struct {
// FieldName is the name of the field that is part of the union. This
// is the serialized form of the field.
FieldName string `yaml:"fieldName"`
// Discriminatorvalue is the value of the discriminator to
// select that field. If the union doesn't have a discriminator,
// this field is ignored.
DiscriminatorValue string `yaml:"discriminatorValue"`
}
// Union, or oneof, means that only one of multiple fields of a structure can be
// set at a time. Setting the discriminator helps clearing oher fields:
// - If discriminator changed to non-nil, and a new field has been added
// that doesn't match, an error is returned,
// - If discriminator hasn't changed and two fields or more are set, an
// error is returned,
// - If discriminator changed to non-nil, all other fields but the
// discriminated one will be cleared,
// - Otherwise, If only one field is left, update discriminator to that value.
type Union struct {
// Discriminator, if present, is the name of the field that
// discriminates fields in the union. The mapping between the value of
// the discriminator and the field is done by using the Fields list
// below.
Discriminator *string `yaml:"discriminator,omitempty"`
// DeduceInvalidDiscriminator indicates if the discriminator
// should be updated automatically based on the fields set. This
// typically defaults to false since we don't want to deduce by
// default (the behavior exists to maintain compatibility on
// existing types and shouldn't be used for new types).
DeduceInvalidDiscriminator bool `yaml:"deduceInvalidDiscriminator,omitempty"`
// This is the list of fields that belong to this union. All the
// fields present in here have to be part of the parent
// structure. Discriminator (if oneOf has one), is NOT included in
// this list. The value for field is how we map the name of the field
// to actual value for discriminator.
Fields []UnionField `yaml:"fields,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"`
// Default value for the field, nil if not present.
Default interface{} `yaml:"default,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 map, the list is treated as a map.
// 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
// map, then Keys must have non-zero length, and it lists the fields
// of the element's map 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"`
}
// 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) {
s.once.Do(func() {
sm := make(map[string]TypeDef, len(s.Types))
for _, t := range s.Types {
sm[t.Name] = t
}
s.m.Store(&sm)
})
t, ok := (*s.m.Load())[name]
return t, ok
}
func (s *Schema) resolveNoOverrides(tr TypeRef) (Atom, bool) {
result := Atom{}
if tr.NamedType != nil {
t, ok := s.FindNamedType(*tr.NamedType)
if !ok {
return Atom{}, false
}
result = t.Atom
} else {
result = tr.Inlined
}
return result, true
}
// 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 this is a plain reference with no overrides, just return the type
if tr.ElementRelationship == nil {
return s.resolveNoOverrides(tr)
}
s.lock.Lock()
defer s.lock.Unlock()
if s.resolvedTypes == nil {
s.resolvedTypes = make(map[TypeRef]Atom)
}
var result Atom
var exists bool
// Return cached result if available
// If not, calculate result and cache it
if result, exists = s.resolvedTypes[tr]; !exists {
if result, exists = s.resolveNoOverrides(tr); exists {
// Allow field-level electives to override the referred type's modifiers
switch {
case result.Map != nil:
mapCopy := Map{}
result.Map.CopyInto(&mapCopy)
mapCopy.ElementRelationship = *tr.ElementRelationship
result.Map = &mapCopy
case result.List != nil:
listCopy := *result.List
listCopy.ElementRelationship = *tr.ElementRelationship
result.List = &listCopy
case result.Scalar != nil:
return Atom{}, false
default:
return Atom{}, false
}
} else {
return Atom{}, false
}
// Save result. If it is nil, that is also recorded as not existing.
s.resolvedTypes[tr] = result
}
return result, true
}
// CopyInto clones this instance of Schema into dst
//
// If dst is nil this method does nothing.
// If dst is already initialized, overwrites it with this instance.
// Warning: Not thread safe. Only use dst after this function returns.
func (s *Schema) CopyInto(dst *Schema) {
if dst == nil {
return
}
// Schema type is considered immutable so sharing references
dst.Types = s.Types
sm := s.m.Load()
if sm != nil {
// If cache is non-nil then the once token had been consumed.
// Must reset token and use it again to ensure same semantics.
dst.once = sync.Once{}
dst.once.Do(func() {
dst.m.Store(sm)
})
}
}
+202
View File
@@ -0,0 +1,202 @@
/*
Copyright 2019 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
import "reflect"
// Equals returns true iff the two Schemas are equal.
func (a *Schema) Equals(b *Schema) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if len(a.Types) != len(b.Types) {
return false
}
for i := range a.Types {
if !a.Types[i].Equals(&b.Types[i]) {
return false
}
}
return true
}
// Equals returns true iff the two TypeRefs are equal.
//
// Note that two typerefs that have an equivalent type but where one is
// inlined and the other is named, are not considered equal.
func (a *TypeRef) Equals(b *TypeRef) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if (a.NamedType == nil) != (b.NamedType == nil) {
return false
}
if a.NamedType != nil {
if *a.NamedType != *b.NamedType {
return false
}
//return true
}
if a.ElementRelationship != b.ElementRelationship {
return false
}
return a.Inlined.Equals(&b.Inlined)
}
// Equals returns true iff the two TypeDefs are equal.
func (a *TypeDef) Equals(b *TypeDef) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if a.Name != b.Name {
return false
}
return a.Atom.Equals(&b.Atom)
}
// Equals returns true iff the two Atoms are equal.
func (a *Atom) Equals(b *Atom) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if (a.Scalar == nil) != (b.Scalar == nil) {
return false
}
if (a.List == nil) != (b.List == nil) {
return false
}
if (a.Map == nil) != (b.Map == nil) {
return false
}
switch {
case a.Scalar != nil:
return *a.Scalar == *b.Scalar
case a.List != nil:
return a.List.Equals(b.List)
case a.Map != nil:
return a.Map.Equals(b.Map)
}
return true
}
// Equals returns true iff the two Maps are equal.
func (a *Map) Equals(b *Map) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if !a.ElementType.Equals(&b.ElementType) {
return false
}
if a.ElementRelationship != b.ElementRelationship {
return false
}
if len(a.Fields) != len(b.Fields) {
return false
}
for i := range a.Fields {
if !a.Fields[i].Equals(&b.Fields[i]) {
return false
}
}
if len(a.Unions) != len(b.Unions) {
return false
}
for i := range a.Unions {
if !a.Unions[i].Equals(&b.Unions[i]) {
return false
}
}
return true
}
// Equals returns true iff the two Unions are equal.
func (a *Union) Equals(b *Union) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if (a.Discriminator == nil) != (b.Discriminator == nil) {
return false
}
if a.Discriminator != nil {
if *a.Discriminator != *b.Discriminator {
return false
}
}
if a.DeduceInvalidDiscriminator != b.DeduceInvalidDiscriminator {
return false
}
if len(a.Fields) != len(b.Fields) {
return false
}
for i := range a.Fields {
if !a.Fields[i].Equals(&b.Fields[i]) {
return false
}
}
return true
}
// Equals returns true iff the two UnionFields are equal.
func (a *UnionField) Equals(b *UnionField) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if a.FieldName != b.FieldName {
return false
}
if a.DiscriminatorValue != b.DiscriminatorValue {
return false
}
return true
}
// Equals returns true iff the two StructFields are equal.
func (a *StructField) Equals(b *StructField) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if a.Name != b.Name {
return false
}
if !reflect.DeepEqual(a.Default, b.Default) {
return false
}
return a.Type.Equals(&b.Type)
}
// Equals returns true iff the two Lists are equal.
func (a *List) Equals(b *List) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if !a.ElementType.Equals(&b.ElementType) {
return false
}
if a.ElementRelationship != b.ElementRelationship {
return false
}
if len(a.Keys) != len(b.Keys) {
return false
}
for i := range a.Keys {
if a.Keys[i] != b.Keys[i] {
return false
}
}
return true
}
+165
View File
@@ -0,0 +1,165 @@
/*
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
map:
fields:
- name: types
type:
list:
elementRelationship: associative
elementType:
namedType: typeDef
keys:
- name
- name: typeDef
map:
fields:
- name: name
type:
scalar: string
- name: scalar
type:
scalar: string
- name: map
type:
namedType: map
- name: list
type:
namedType: list
- name: untyped
type:
namedType: untyped
- name: typeRef
map:
fields:
- name: namedType
type:
scalar: string
- name: scalar
type:
scalar: string
- name: map
type:
namedType: map
- name: list
type:
namedType: list
- name: untyped
type:
namedType: untyped
- name: elementRelationship
type:
scalar: string
- name: scalar
scalar: string
- name: map
map:
fields:
- name: fields
type:
list:
elementType:
namedType: structField
elementRelationship: associative
keys: [ "name" ]
- name: unions
type:
list:
elementType:
namedType: union
elementRelationship: atomic
- name: elementType
type:
namedType: typeRef
- name: elementRelationship
type:
scalar: string
- name: unionField
map:
fields:
- name: fieldName
type:
scalar: string
- name: discriminatorValue
type:
scalar: string
- name: union
map:
fields:
- name: discriminator
type:
scalar: string
- name: deduceInvalidDiscriminator
type:
scalar: boolean
- name: fields
type:
list:
elementRelationship: associative
elementType:
namedType: unionField
keys:
- fieldName
- name: structField
map:
fields:
- name: name
type:
scalar: string
- name: type
type:
namedType: typeRef
- name: default
type:
namedType: __untyped_atomic_
- name: list
map:
fields:
- name: elementType
type:
namedType: typeRef
- name: elementRelationship
type:
scalar: string
- name: keys
type:
list:
elementType:
scalar: string
elementRelationship: atomic
- name: untyped
map:
fields:
- name: elementRelationship
type:
scalar: string
- name: __untyped_atomic_
scalar: untyped
list:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
map:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
`
+470
View File
@@ -0,0 +1,470 @@
/*
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"
"strings"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/schema"
"sigs.k8s.io/structured-merge-diff/v6/value"
)
// 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 {
// 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 {
bld := strings.Builder{}
if !c.Modified.Empty() {
bld.WriteString(fmt.Sprintf("- Modified Fields:\n%v\n", c.Modified))
}
if !c.Added.Empty() {
bld.WriteString(fmt.Sprintf("- Added Fields:\n%v\n", c.Added))
}
if !c.Removed.Empty() {
bld.WriteString(fmt.Sprintf("- Removed Fields:\n%v\n", c.Removed))
}
return bld.String()
}
// ExcludeFields fields from the compare recursively removes the fields
// from the entire comparison
func (c *Comparison) ExcludeFields(fields *fieldpath.Set) *Comparison {
if fields == nil || fields.Empty() {
return c
}
c.Removed = c.Removed.RecursiveDifference(fields)
c.Modified = c.Modified.RecursiveDifference(fields)
c.Added = c.Added.RecursiveDifference(fields)
return c
}
func (c *Comparison) FilterFields(filter fieldpath.Filter) *Comparison {
if filter == nil {
return c
}
c.Removed = filter.Filter(c.Removed)
c.Modified = filter.Filter(c.Modified)
c.Added = filter.Filter(c.Added)
return c
}
type compareWalker struct {
lhs value.Value
rhs value.Value
schema *schema.Schema
typeRef schema.TypeRef
// Current path that we are comparing
path fieldpath.Path
// Resulting comparison.
comparison *Comparison
// internal housekeeping--don't set when constructing.
inLeaf bool // Set to true if we're in a "big leaf"--atomic map/list
// Allocate only as many walkers as needed for the depth by storing them here.
spareWalkers *[]*compareWalker
allocator value.Allocator
}
// compare compares stuff.
func (w *compareWalker) compare(prefixFn func() string) (errs ValidationErrors) {
if w.lhs == nil && w.rhs == nil {
// check this condidition here instead of everywhere below.
return errorf("at least one of lhs and rhs must be provided")
}
a, ok := w.schema.Resolve(w.typeRef)
if !ok {
return errorf("schema error: no type found matching: %v", *w.typeRef.NamedType)
}
alhs := deduceAtom(a, w.lhs)
arhs := deduceAtom(a, w.rhs)
// deduceAtom does not fix the type for nil values
// nil is a wildcard and will accept whatever form the other operand takes
if w.rhs == nil {
errs = append(errs, handleAtom(alhs, w.typeRef, w)...)
} else if w.lhs == nil || alhs.Equals(&arhs) {
errs = append(errs, handleAtom(arhs, w.typeRef, w)...)
} else {
w2 := *w
errs = append(errs, handleAtom(alhs, w.typeRef, &w2)...)
errs = append(errs, handleAtom(arhs, w.typeRef, w)...)
}
if !w.inLeaf {
if w.lhs == nil {
w.comparison.Added.Insert(w.path)
} else if w.rhs == nil {
w.comparison.Removed.Insert(w.path)
}
}
return errs.WithLazyPrefix(prefixFn)
}
// doLeaf should be called on leaves before descending into children, if there
// will be a descent. It modifies w.inLeaf.
func (w *compareWalker) doLeaf() {
if w.inLeaf {
// We're in a "big leaf", an atomic map or list. Ignore
// subsequent leaves.
return
}
w.inLeaf = true
// We don't recurse into leaf fields for merging.
if w.lhs == nil {
w.comparison.Added.Insert(w.path)
} else if w.rhs == nil {
w.comparison.Removed.Insert(w.path)
} else if !value.EqualsUsing(w.allocator, w.rhs, w.lhs) {
// TODO: Equality is not sufficient for this.
// Need to implement equality check on the value type.
w.comparison.Modified.Insert(w.path)
}
}
func (w *compareWalker) doScalar(t *schema.Scalar) ValidationErrors {
// Make sure at least one side is a valid scalar.
lerrs := validateScalar(t, w.lhs, "lhs: ")
rerrs := validateScalar(t, w.rhs, "rhs: ")
if len(lerrs) > 0 && len(rerrs) > 0 {
return append(lerrs, rerrs...)
}
// All scalars are leaf fields.
w.doLeaf()
return nil
}
func (w *compareWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef, cmp *Comparison) *compareWalker {
if w.spareWalkers == nil {
// first descent.
w.spareWalkers = &[]*compareWalker{}
}
var w2 *compareWalker
if n := len(*w.spareWalkers); n > 0 {
w2, *w.spareWalkers = (*w.spareWalkers)[n-1], (*w.spareWalkers)[:n-1]
} else {
w2 = &compareWalker{}
}
*w2 = *w
w2.typeRef = tr
w2.path = append(w2.path, pe)
w2.lhs = nil
w2.rhs = nil
w2.comparison = cmp
return w2
}
func (w *compareWalker) finishDescent(w2 *compareWalker) {
// if the descent caused a realloc, ensure that we reuse the buffer
// for the next sibling.
w.path = w2.path[:len(w2.path)-1]
*w.spareWalkers = append(*w.spareWalkers, w2)
}
func (w *compareWalker) derefMap(prefix string, v value.Value) (value.Map, ValidationErrors) {
if v == nil {
return nil, nil
}
m, err := mapValue(w.allocator, v)
if err != nil {
return nil, errorf("%v: %v", prefix, err)
}
return m, nil
}
func (w *compareWalker) visitListItems(t *schema.List, lhs, rhs value.List) (errs ValidationErrors) {
rLen := 0
if rhs != nil {
rLen = rhs.Length()
}
lLen := 0
if lhs != nil {
lLen = lhs.Length()
}
maxLength := rLen
if lLen > maxLength {
maxLength = lLen
}
// Contains all the unique PEs between lhs and rhs, exactly once.
// Order doesn't matter since we're just tracking ownership in a set.
allPEs := make([]fieldpath.PathElement, 0, maxLength)
// Gather all the elements from lhs, indexed by PE, in a list for duplicates.
lValues := fieldpath.MakePathElementMap(lLen)
for i := 0; i < lLen; i++ {
child := lhs.At(i)
pe, err := listItemToPathElement(w.allocator, w.schema, t, child)
if err != nil {
errs = append(errs, 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
}
if v, found := lValues.Get(pe); found {
list := v.([]value.Value)
lValues.Insert(pe, append(list, child))
} else {
lValues.Insert(pe, []value.Value{child})
allPEs = append(allPEs, pe)
}
}
// Gather all the elements from rhs, indexed by PE, in a list for duplicates.
rValues := fieldpath.MakePathElementMap(rLen)
for i := 0; i < rLen; i++ {
rValue := rhs.At(i)
pe, err := listItemToPathElement(w.allocator, w.schema, t, rValue)
if err != nil {
errs = append(errs, 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
}
if v, found := rValues.Get(pe); found {
list := v.([]value.Value)
rValues.Insert(pe, append(list, rValue))
} else {
rValues.Insert(pe, []value.Value{rValue})
if _, found := lValues.Get(pe); !found {
allPEs = append(allPEs, pe)
}
}
}
for _, pe := range allPEs {
lList := []value.Value(nil)
if l, ok := lValues.Get(pe); ok {
lList = l.([]value.Value)
}
rList := []value.Value(nil)
if l, ok := rValues.Get(pe); ok {
rList = l.([]value.Value)
}
switch {
case len(lList) == 0 && len(rList) == 0:
// We shouldn't be here anyway.
return
// Normal use-case:
// We have no duplicates for this PE, compare items one-to-one.
case len(lList) <= 1 && len(rList) <= 1:
lValue := value.Value(nil)
if len(lList) != 0 {
lValue = lList[0]
}
rValue := value.Value(nil)
if len(rList) != 0 {
rValue = rList[0]
}
errs = append(errs, w.compareListItem(t, pe, lValue, rValue)...)
// Duplicates before & after use-case:
// Compare the duplicates lists as if they were atomic, mark modified if they changed.
case len(lList) >= 2 && len(rList) >= 2:
listEqual := func(lList, rList []value.Value) bool {
if len(lList) != len(rList) {
return false
}
for i := range lList {
if !value.Equals(lList[i], rList[i]) {
return false
}
}
return true
}
if !listEqual(lList, rList) {
w.comparison.Modified.Insert(append(w.path, pe))
}
// Duplicates before & not anymore use-case:
// Rcursively add new non-duplicate items, Remove duplicate marker,
case len(lList) >= 2:
if len(rList) != 0 {
errs = append(errs, w.compareListItem(t, pe, nil, rList[0])...)
}
w.comparison.Removed.Insert(append(w.path, pe))
// New duplicates use-case:
// Recursively remove old non-duplicate items, add duplicate marker.
case len(rList) >= 2:
if len(lList) != 0 {
errs = append(errs, w.compareListItem(t, pe, lList[0], nil)...)
}
w.comparison.Added.Insert(append(w.path, pe))
}
}
return
}
func (w *compareWalker) indexListPathElements(t *schema.List, list value.List) ([]fieldpath.PathElement, fieldpath.PathElementValueMap, ValidationErrors) {
var errs ValidationErrors
length := 0
if list != nil {
length = list.Length()
}
observed := fieldpath.MakePathElementValueMap(length)
pes := make([]fieldpath.PathElement, 0, length)
for i := 0; i < length; i++ {
child := list.At(i)
pe, err := listItemToPathElement(w.allocator, w.schema, t, child)
if err != nil {
errs = append(errs, 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
}
// Ignore repeated occurences of `pe`.
if _, found := observed.Get(pe); found {
continue
}
observed.Insert(pe, child)
pes = append(pes, pe)
}
return pes, observed, errs
}
func (w *compareWalker) compareListItem(t *schema.List, pe fieldpath.PathElement, lChild, rChild value.Value) ValidationErrors {
w2 := w.prepareDescent(pe, t.ElementType, w.comparison)
w2.lhs = lChild
w2.rhs = rChild
errs := w2.compare(pe.String)
w.finishDescent(w2)
return errs
}
func (w *compareWalker) derefList(prefix string, v value.Value) (value.List, ValidationErrors) {
if v == nil {
return nil, nil
}
l, err := listValue(w.allocator, v)
if err != nil {
return nil, errorf("%v: %v", prefix, err)
}
return l, nil
}
func (w *compareWalker) doList(t *schema.List) (errs ValidationErrors) {
lhs, _ := w.derefList("lhs: ", w.lhs)
if lhs != nil {
defer w.allocator.Free(lhs)
}
rhs, _ := w.derefList("rhs: ", w.rhs)
if rhs != nil {
defer w.allocator.Free(rhs)
}
// If both lhs and rhs are empty/null, treat it as a
// leaf: this helps preserve the empty/null
// distinction.
emptyPromoteToLeaf := (lhs == nil || lhs.Length() == 0) && (rhs == nil || rhs.Length() == 0)
if t.ElementRelationship == schema.Atomic || emptyPromoteToLeaf {
w.doLeaf()
return nil
}
if lhs == nil && rhs == nil {
return nil
}
errs = w.visitListItems(t, lhs, rhs)
return errs
}
func (w *compareWalker) visitMapItem(t *schema.Map, out map[string]interface{}, key string, lhs, rhs value.Value) (errs ValidationErrors) {
fieldType := t.ElementType
if sf, ok := t.FindField(key); ok {
fieldType = sf.Type
}
pe := fieldpath.PathElement{FieldName: &key}
w2 := w.prepareDescent(pe, fieldType, w.comparison)
w2.lhs = lhs
w2.rhs = rhs
errs = append(errs, w2.compare(pe.String)...)
w.finishDescent(w2)
return errs
}
func (w *compareWalker) visitMapItems(t *schema.Map, lhs, rhs value.Map) (errs ValidationErrors) {
out := map[string]interface{}{}
value.MapZipUsing(w.allocator, lhs, rhs, value.Unordered, func(key string, lhsValue, rhsValue value.Value) bool {
errs = append(errs, w.visitMapItem(t, out, key, lhsValue, rhsValue)...)
return true
})
return errs
}
func (w *compareWalker) doMap(t *schema.Map) (errs ValidationErrors) {
lhs, _ := w.derefMap("lhs: ", w.lhs)
if lhs != nil {
defer w.allocator.Free(lhs)
}
rhs, _ := w.derefMap("rhs: ", w.rhs)
if rhs != nil {
defer w.allocator.Free(rhs)
}
// If both lhs and rhs are empty/null, treat it as a
// leaf: this helps preserve the empty/null
// distinction.
emptyPromoteToLeaf := (lhs == nil || lhs.Empty()) && (rhs == nil || rhs.Empty())
if t.ElementRelationship == schema.Atomic || emptyPromoteToLeaf {
w.doLeaf()
return nil
}
if lhs == nil && rhs == nil {
return nil
}
errs = append(errs, w.visitMapItems(t, lhs, rhs)...)
return errs
}
+18
View File
@@ -0,0 +1,18 @@
/*
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
+266
View File
@@ -0,0 +1,266 @@
/*
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/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/schema"
"sigs.k8s.io/structured-merge-diff/v6/value"
)
// ValidationError reports an error about a particular field
type ValidationError struct {
Path string
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")
}
// Set the given path to all the validation errors.
func (errs ValidationErrors) WithPath(p string) ValidationErrors {
for i := range errs {
errs[i].Path = p
}
return errs
}
// WithPrefix prefixes all errors path with the given pathelement. This
// is useful when unwinding the stack on errors.
func (errs ValidationErrors) WithPrefix(prefix string) ValidationErrors {
for i := range errs {
errs[i].Path = prefix + errs[i].Path
}
return errs
}
// WithLazyPrefix prefixes all errors path with the given pathelement.
// This is useful when unwinding the stack on errors. Prefix is
// computed lazily only if there is an error.
func (errs ValidationErrors) WithLazyPrefix(fn func() string) ValidationErrors {
if len(errs) == 0 {
return errs
}
prefix := ""
if fn != nil {
prefix = fn()
}
for i := range errs {
errs[i].Path = prefix + errs[i].Path
}
return errs
}
func errorf(format string, args ...interface{}) ValidationErrors {
return ValidationErrors{{
ErrorMessage: fmt.Sprintf(format, args...),
}}
}
type atomHandler interface {
doScalar(*schema.Scalar) ValidationErrors
doList(*schema.List) ValidationErrors
doMap(*schema.Map) ValidationErrors
}
func resolveSchema(s *schema.Schema, tr schema.TypeRef, v value.Value, ah atomHandler) ValidationErrors {
a, ok := s.Resolve(tr)
if !ok {
typeName := "inlined type"
if tr.NamedType != nil {
typeName = *tr.NamedType
}
return errorf("schema error: no type found matching: %v", typeName)
}
a = deduceAtom(a, v)
return handleAtom(a, tr, ah)
}
// deduceAtom determines which of the possible types in atom 'atom' applies to value 'val'.
// If val is of a type allowed by atom, return a copy of atom with all other types set to nil.
// if val is nil, or is not of a type allowed by atom, just return the original atom,
// and validation will fail at a later stage. (with a more useful error)
func deduceAtom(atom schema.Atom, val value.Value) schema.Atom {
switch {
case val == nil:
case val.IsFloat(), val.IsInt(), val.IsString(), val.IsBool():
if atom.Scalar != nil {
return schema.Atom{Scalar: atom.Scalar}
}
case val.IsList():
if atom.List != nil {
return schema.Atom{List: atom.List}
}
case val.IsMap():
if atom.Map != nil {
return schema.Atom{Map: atom.Map}
}
}
return atom
}
func handleAtom(a schema.Atom, tr schema.TypeRef, ah atomHandler) ValidationErrors {
switch {
case a.Map != nil:
return ah.doMap(a.Map)
case a.Scalar != nil:
return ah.doScalar(a.Scalar)
case a.List != nil:
return ah.doList(a.List)
}
name := "inlined"
if tr.NamedType != nil {
name = "named type: " + *tr.NamedType
}
return errorf("schema error: invalid atom: %v", name)
}
// Returns the list, or an error. Reminder: nil is a valid list and might be returned.
func listValue(a value.Allocator, val value.Value) (value.List, error) {
if val.IsNull() {
// Null is a valid list.
return nil, nil
}
if !val.IsList() {
return nil, fmt.Errorf("expected list, got %v", val)
}
return val.AsListUsing(a), nil
}
// Returns the map, or an error. Reminder: nil is a valid map and might be returned.
func mapValue(a value.Allocator, val value.Value) (value.Map, error) {
if val == nil {
return nil, fmt.Errorf("expected map, got nil")
}
if val.IsNull() {
// Null is a valid map.
return nil, nil
}
if !val.IsMap() {
return nil, fmt.Errorf("expected map, got %v", val)
}
return val.AsMapUsing(a), nil
}
func getAssociativeKeyDefault(s *schema.Schema, list *schema.List, fieldName string) (interface{}, error) {
atom, ok := s.Resolve(list.ElementType)
if !ok {
return nil, errors.New("invalid elementType for list")
}
if atom.Map == nil {
return nil, errors.New("associative list may not have non-map types")
}
// If the field is not found, we can assume there is no default.
field, _ := atom.Map.FindField(fieldName)
return field.Default, nil
}
func keyedAssociativeListItemToPathElement(a value.Allocator, s *schema.Schema, list *schema.List, child value.Value) (fieldpath.PathElement, error) {
pe := fieldpath.PathElement{}
if child.IsNull() {
// null entries are illegal.
return pe, errors.New("associative list with keys may not have a null element")
}
if !child.IsMap() {
return pe, errors.New("associative list with keys may not have non-map elements")
}
keyMap := value.FieldList{}
m := child.AsMapUsing(a)
defer a.Free(m)
for _, fieldName := range list.Keys {
if val, ok := m.Get(fieldName); ok {
keyMap = append(keyMap, value.Field{Name: fieldName, Value: val})
} else if def, err := getAssociativeKeyDefault(s, list, fieldName); err != nil {
return pe, fmt.Errorf("couldn't find default value for %v: %v", fieldName, err)
} else if def != nil {
keyMap = append(keyMap, value.Field{Name: fieldName, Value: value.NewValueInterface(def)})
} else {
// Don't add the key to the key field list.
// A key field list where it is set then represents a different entry
// in the associate list.
}
}
if len(list.Keys) > 0 && len(keyMap) == 0 {
return pe, fmt.Errorf("associative list with keys has an element that omits all key fields %q (and doesn't have default values for any key fields)", list.Keys)
}
keyMap.Sort()
pe.Key = &keyMap
return pe, nil
}
func setItemToPathElement(child value.Value) (fieldpath.PathElement, error) {
pe := fieldpath.PathElement{}
switch {
case child.IsMap():
// TODO: atomic maps should be acceptable.
return pe, errors.New("associative list without keys has an element that's a map type")
case child.IsList():
// 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.IsNull():
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(a value.Allocator, s *schema.Schema, list *schema.List, child value.Value) (fieldpath.PathElement, error) {
if list.ElementRelationship != schema.Associative {
return fieldpath.PathElement{}, errors.New("invalid indexing of non-associative list")
}
if len(list.Keys) > 0 {
return keyedAssociativeListItemToPathElement(a, s, list, child)
}
// If there's no keys, then we must be a set of primitives.
return setItemToPathElement(child)
}
+427
View File
@@ -0,0 +1,427 @@
/*
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/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/schema"
"sigs.k8s.io/structured-merge-diff/v6/value"
)
type mergingWalker struct {
lhs value.Value
rhs value.Value
schema *schema.Schema
typeRef schema.TypeRef
// Current path that we are merging
path fieldpath.Path
// How to merge. Called after schema validation for all leaf fields.
rule mergeRule
// If set, called after non-leaf items have been merged. (`out` is
// probably already set.)
postItemHook mergeRule
// output of the merge operation (nil if none)
out *interface{}
// internal housekeeping--don't set when constructing.
inLeaf bool // Set to true if we're in a "big leaf"--atomic map/list
// Allocate only as many walkers as needed for the depth by storing them here.
spareWalkers *[]*mergingWalker
allocator value.Allocator
}
// merge rules examine w.lhs and w.rhs (up to one of which may be nil) and
// optionally set w.out. If lhs and rhs are both set, they will be of
// comparable type.
type mergeRule func(w *mergingWalker)
var (
ruleKeepRHS = mergeRule(func(w *mergingWalker) {
if w.rhs != nil {
v := w.rhs.Unstructured()
w.out = &v
} else if w.lhs != nil {
v := w.lhs.Unstructured()
w.out = &v
}
})
)
// merge sets w.out.
func (w *mergingWalker) merge(prefixFn func() string) (errs ValidationErrors) {
if w.lhs == nil && w.rhs == nil {
// check this condidition here instead of everywhere below.
return errorf("at least one of lhs and rhs must be provided")
}
a, ok := w.schema.Resolve(w.typeRef)
if !ok {
return errorf("schema error: no type found matching: %v", *w.typeRef.NamedType)
}
alhs := deduceAtom(a, w.lhs)
arhs := deduceAtom(a, w.rhs)
// deduceAtom does not fix the type for nil values
// nil is a wildcard and will accept whatever form the other operand takes
if w.rhs == nil {
errs = append(errs, handleAtom(alhs, w.typeRef, w)...)
} else if w.lhs == nil || alhs.Equals(&arhs) {
errs = append(errs, handleAtom(arhs, w.typeRef, w)...)
} else {
w2 := *w
errs = append(errs, handleAtom(alhs, w.typeRef, &w2)...)
errs = append(errs, handleAtom(arhs, w.typeRef, w)...)
}
if !w.inLeaf && w.postItemHook != nil {
w.postItemHook(w)
}
return errs.WithLazyPrefix(prefixFn)
}
// doLeaf should be called on leaves before descending into children, if there
// will be a descent. It modifies w.inLeaf.
func (w *mergingWalker) doLeaf() {
if w.inLeaf {
// We're in a "big leaf", an atomic map or list. Ignore
// subsequent leaves.
return
}
w.inLeaf = true
// We don't recurse into leaf fields for merging.
w.rule(w)
}
func (w *mergingWalker) doScalar(t *schema.Scalar) ValidationErrors {
// Make sure at least one side is a valid scalar.
lerrs := validateScalar(t, w.lhs, "lhs: ")
rerrs := validateScalar(t, w.rhs, "rhs: ")
if len(lerrs) > 0 && len(rerrs) > 0 {
return append(lerrs, rerrs...)
}
// All scalars are leaf fields.
w.doLeaf()
return nil
}
func (w *mergingWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef) *mergingWalker {
if w.spareWalkers == nil {
// first descent.
w.spareWalkers = &[]*mergingWalker{}
}
var w2 *mergingWalker
if n := len(*w.spareWalkers); n > 0 {
w2, *w.spareWalkers = (*w.spareWalkers)[n-1], (*w.spareWalkers)[:n-1]
} else {
w2 = &mergingWalker{}
}
*w2 = *w
w2.typeRef = tr
w2.path = append(w2.path, pe)
w2.lhs = nil
w2.rhs = nil
w2.out = nil
return w2
}
func (w *mergingWalker) finishDescent(w2 *mergingWalker) {
// if the descent caused a realloc, ensure that we reuse the buffer
// for the next sibling.
w.path = w2.path[:len(w2.path)-1]
*w.spareWalkers = append(*w.spareWalkers, w2)
}
func (w *mergingWalker) derefMap(prefix string, v value.Value) (value.Map, ValidationErrors) {
if v == nil {
return nil, nil
}
m, err := mapValue(w.allocator, v)
if err != nil {
return nil, errorf("%v: %v", prefix, err)
}
return m, nil
}
func (w *mergingWalker) visitListItems(t *schema.List, lhs, rhs value.List) (errs ValidationErrors) {
rLen := 0
if rhs != nil {
rLen = rhs.Length()
}
lLen := 0
if lhs != nil {
lLen = lhs.Length()
}
outLen := lLen
if outLen < rLen {
outLen = rLen
}
out := make([]interface{}, 0, outLen)
rhsPEs, observedRHS, rhsErrs := w.indexListPathElements(t, rhs, false)
errs = append(errs, rhsErrs...)
lhsPEs, observedLHS, lhsErrs := w.indexListPathElements(t, lhs, true)
errs = append(errs, lhsErrs...)
if len(errs) != 0 {
return errs
}
sharedOrder := make([]*fieldpath.PathElement, 0, rLen)
for i := range rhsPEs {
pe := &rhsPEs[i]
if _, ok := observedLHS.Get(*pe); ok {
sharedOrder = append(sharedOrder, pe)
}
}
var nextShared *fieldpath.PathElement
if len(sharedOrder) > 0 {
nextShared = sharedOrder[0]
sharedOrder = sharedOrder[1:]
}
mergedRHS := fieldpath.MakePathElementMap(len(rhsPEs))
lLen, rLen = len(lhsPEs), len(rhsPEs)
for lI, rI := 0, 0; lI < lLen || rI < rLen; {
if lI < lLen && rI < rLen {
pe := lhsPEs[lI]
if pe.Equals(rhsPEs[rI]) {
// merge LHS & RHS items
mergedRHS.Insert(pe, struct{}{})
lChild, _ := observedLHS.Get(pe) // may be nil if the PE is duplicaated.
rChild, _ := observedRHS.Get(pe)
mergeOut, errs := w.mergeListItem(t, pe, lChild, rChild)
errs = append(errs, errs...)
if mergeOut != nil {
out = append(out, *mergeOut)
}
lI++
rI++
nextShared = nil
if len(sharedOrder) > 0 {
nextShared = sharedOrder[0]
sharedOrder = sharedOrder[1:]
}
continue
}
if _, ok := observedRHS.Get(pe); ok && nextShared != nil && !nextShared.Equals(lhsPEs[lI]) {
// shared item, but not the one we want in this round
lI++
continue
}
}
if lI < lLen {
pe := lhsPEs[lI]
if _, ok := observedRHS.Get(pe); !ok {
// take LHS item using At to make sure we get the right item (observed may not contain the right item).
lChild := lhs.AtUsing(w.allocator, lI)
mergeOut, errs := w.mergeListItem(t, pe, lChild, nil)
errs = append(errs, errs...)
if mergeOut != nil {
out = append(out, *mergeOut)
}
lI++
continue
} else if _, ok := mergedRHS.Get(pe); ok {
// we've already merged it with RHS, we don't want to duplicate it, skip it.
lI++
}
}
if rI < rLen {
// Take the RHS item, merge with matching LHS item if possible
pe := rhsPEs[rI]
mergedRHS.Insert(pe, struct{}{})
lChild, _ := observedLHS.Get(pe) // may be nil if absent or duplicaated.
rChild, _ := observedRHS.Get(pe)
mergeOut, errs := w.mergeListItem(t, pe, lChild, rChild)
errs = append(errs, errs...)
if mergeOut != nil {
out = append(out, *mergeOut)
}
rI++
// Advance nextShared, if we are merging nextShared.
if nextShared != nil && nextShared.Equals(pe) {
nextShared = nil
if len(sharedOrder) > 0 {
nextShared = sharedOrder[0]
sharedOrder = sharedOrder[1:]
}
}
}
}
if len(out) > 0 {
i := interface{}(out)
w.out = &i
}
return errs
}
func (w *mergingWalker) indexListPathElements(t *schema.List, list value.List, allowDuplicates bool) ([]fieldpath.PathElement, fieldpath.PathElementValueMap, ValidationErrors) {
var errs ValidationErrors
length := 0
if list != nil {
length = list.Length()
}
observed := fieldpath.MakePathElementValueMap(length)
pes := make([]fieldpath.PathElement, 0, length)
for i := 0; i < length; i++ {
child := list.At(i)
pe, err := listItemToPathElement(w.allocator, w.schema, t, child)
if err != nil {
errs = append(errs, 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
}
if _, found := observed.Get(pe); found && !allowDuplicates {
errs = append(errs, errorf("duplicate entries for key %v", pe.String())...)
continue
} else if !found {
observed.Insert(pe, child)
} else {
// Duplicated items are not merged with the new value, make them nil.
observed.Insert(pe, value.NewValueInterface(nil))
}
pes = append(pes, pe)
}
return pes, observed, errs
}
func (w *mergingWalker) mergeListItem(t *schema.List, pe fieldpath.PathElement, lChild, rChild value.Value) (out *interface{}, errs ValidationErrors) {
w2 := w.prepareDescent(pe, t.ElementType)
w2.lhs = lChild
w2.rhs = rChild
errs = append(errs, w2.merge(pe.String)...)
if w2.out != nil {
out = w2.out
}
w.finishDescent(w2)
return
}
func (w *mergingWalker) derefList(prefix string, v value.Value) (value.List, ValidationErrors) {
if v == nil {
return nil, nil
}
l, err := listValue(w.allocator, v)
if err != nil {
return nil, errorf("%v: %v", prefix, err)
}
return l, nil
}
func (w *mergingWalker) doList(t *schema.List) (errs ValidationErrors) {
lhs, _ := w.derefList("lhs: ", w.lhs)
if lhs != nil {
defer w.allocator.Free(lhs)
}
rhs, _ := w.derefList("rhs: ", w.rhs)
if rhs != nil {
defer w.allocator.Free(rhs)
}
// If both lhs and rhs are empty/null, treat it as a
// leaf: this helps preserve the empty/null
// distinction.
emptyPromoteToLeaf := (lhs == nil || lhs.Length() == 0) && (rhs == nil || rhs.Length() == 0)
if t.ElementRelationship == schema.Atomic || emptyPromoteToLeaf {
w.doLeaf()
return nil
}
if lhs == nil && rhs == nil {
return nil
}
errs = w.visitListItems(t, lhs, rhs)
return errs
}
func (w *mergingWalker) visitMapItem(t *schema.Map, out map[string]interface{}, key string, lhs, rhs value.Value) (errs ValidationErrors) {
fieldType := t.ElementType
if sf, ok := t.FindField(key); ok {
fieldType = sf.Type
}
pe := fieldpath.PathElement{FieldName: &key}
w2 := w.prepareDescent(pe, fieldType)
w2.lhs = lhs
w2.rhs = rhs
errs = append(errs, w2.merge(pe.String)...)
if w2.out != nil {
out[key] = *w2.out
}
w.finishDescent(w2)
return errs
}
func (w *mergingWalker) visitMapItems(t *schema.Map, lhs, rhs value.Map) (errs ValidationErrors) {
out := map[string]interface{}{}
value.MapZipUsing(w.allocator, lhs, rhs, value.Unordered, func(key string, lhsValue, rhsValue value.Value) bool {
errs = append(errs, w.visitMapItem(t, out, key, lhsValue, rhsValue)...)
return true
})
if len(out) > 0 {
i := interface{}(out)
w.out = &i
}
return errs
}
func (w *mergingWalker) doMap(t *schema.Map) (errs ValidationErrors) {
lhs, _ := w.derefMap("lhs: ", w.lhs)
if lhs != nil {
defer w.allocator.Free(lhs)
}
rhs, _ := w.derefMap("rhs: ", w.rhs)
if rhs != nil {
defer w.allocator.Free(rhs)
}
// If both lhs and rhs are empty/null, treat it as a
// leaf: this helps preserve the empty/null
// distinction.
emptyPromoteToLeaf := (lhs == nil || lhs.Empty()) && (rhs == nil || rhs.Empty())
if t.ElementRelationship == schema.Atomic || emptyPromoteToLeaf {
w.doLeaf()
return nil
}
if lhs == nil && rhs == nil {
return nil
}
errs = append(errs, w.visitMapItems(t, lhs, rhs)...)
return errs
}
+151
View File
@@ -0,0 +1,151 @@
/*
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 "go.yaml.in/yaml/v2"
"sigs.k8s.io/structured-merge-diff/v6/schema"
"sigs.k8s.io/structured-merge-diff/v6/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(s YAMLObject) (*Parser, error) {
p := Parser{}
err := yaml.Unmarshal([]byte(s), &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)
}
p, err := create(schema)
if err != nil {
return nil, err
}
return p, nil
}
// 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{
Schema: &p.Schema,
TypeRef: schema.TypeRef{NamedType: &name},
}
}
// ParseableType allows for easy production of typed objects.
type ParseableType struct {
TypeRef schema.TypeRef
Schema *schema.Schema
}
// IsValid return true if p's schema and typename are valid.
func (p ParseableType) IsValid() bool {
_, ok := p.Schema.Resolve(p.TypeRef)
return ok
}
// 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, opts ...ValidationOptions) (*TypedValue, error) {
var v interface{}
err := yaml.Unmarshal([]byte(object), &v)
if err != nil {
return nil, err
}
return AsTyped(value.NewValueInterface(v), p.Schema, p.TypeRef, opts...)
}
// FromUnstructured converts a go "interface{}" type, typically an
// unstructured object in Kubernetes world, to a TypedValue. It returns an
// error if the resulting object fails schema validation.
// The provided interface{} must be one of: map[string]interface{},
// map[interface{}]interface{}, []interface{}, int types, float types,
// string or boolean. Nested interface{} must also be one of these types.
func (p ParseableType) FromUnstructured(in interface{}, opts ...ValidationOptions) (*TypedValue, error) {
return AsTyped(value.NewValueInterface(in), p.Schema, p.TypeRef, opts...)
}
// FromStructured converts a go "interface{}" type, typically an structured object in
// Kubernetes, to a TypedValue. It will return an error if the resulting object fails
// schema validation. The provided "interface{}" value must be a pointer so that the
// value can be modified via reflection. The provided "interface{}" may contain structs
// and types that are converted to Values by the jsonMarshaler interface.
func (p ParseableType) FromStructured(in interface{}, opts ...ValidationOptions) (*TypedValue, error) {
v, err := value.NewValueReflect(in)
if err != nil {
return nil, fmt.Errorf("error creating struct value reflector: %v", err)
}
return AsTyped(v, p.Schema, p.TypeRef, opts...)
}
// DeducedParseableType is a ParseableType that deduces the type from
// the content of the object.
var DeducedParseableType ParseableType = createOrDie(YAMLObject(`types:
- name: __untyped_atomic_
scalar: untyped
list:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
map:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
- name: __untyped_deduced_
scalar: untyped
list:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
map:
elementType:
namedType: __untyped_deduced_
elementRelationship: separable
`)).Type("__untyped_deduced_")
+290
View File
@@ -0,0 +1,290 @@
/*
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"
"sync"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/schema"
)
var fmPool = sync.Pool{
New: func() interface{} { return &reconcileWithSchemaWalker{} },
}
func (v *reconcileWithSchemaWalker) finished() {
v.fieldSet = nil
v.schema = nil
v.value = nil
v.typeRef = schema.TypeRef{}
v.path = nil
v.toRemove = nil
v.toAdd = nil
fmPool.Put(v)
}
type reconcileWithSchemaWalker struct {
value *TypedValue // root of the live object
schema *schema.Schema // root of the live schema
// state of node being visited by walker
fieldSet *fieldpath.Set
typeRef schema.TypeRef
path fieldpath.Path
isAtomic bool
// the accumulated diff to perform to apply reconciliation
toRemove *fieldpath.Set // paths to remove recursively
toAdd *fieldpath.Set // paths to add after any removals
// Allocate only as many walkers as needed for the depth by storing them here.
spareWalkers *[]*reconcileWithSchemaWalker
}
func (v *reconcileWithSchemaWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef) *reconcileWithSchemaWalker {
if v.spareWalkers == nil {
// first descent.
v.spareWalkers = &[]*reconcileWithSchemaWalker{}
}
var v2 *reconcileWithSchemaWalker
if n := len(*v.spareWalkers); n > 0 {
v2, *v.spareWalkers = (*v.spareWalkers)[n-1], (*v.spareWalkers)[:n-1]
} else {
v2 = &reconcileWithSchemaWalker{}
}
*v2 = *v
v2.typeRef = tr
v2.path = append(v.path, pe)
v2.value = v.value
return v2
}
func (v *reconcileWithSchemaWalker) finishDescent(v2 *reconcileWithSchemaWalker) {
v2.fieldSet = nil
v2.schema = nil
v2.value = nil
v2.typeRef = schema.TypeRef{}
if cap(v2.path) < 20 { // recycle slices that do not have unexpectedly high capacity
v2.path = v2.path[:0]
} else {
v2.path = nil
}
// merge any accumulated changes into parent walker
if v2.toRemove != nil {
if v.toRemove == nil {
v.toRemove = v2.toRemove
} else {
v.toRemove = v.toRemove.Union(v2.toRemove)
}
}
if v2.toAdd != nil {
if v.toAdd == nil {
v.toAdd = v2.toAdd
} else {
v.toAdd = v.toAdd.Union(v2.toAdd)
}
}
v2.toRemove = nil
v2.toAdd = nil
// if the descent caused a realloc, ensure that we reuse the buffer
// for the next sibling.
*v.spareWalkers = append(*v.spareWalkers, v2)
}
// ReconcileFieldSetWithSchema reconciles the a field set with any changes to the
// object's schema since the field set was written. Returns the reconciled field set, or nil of
// no changes were made to the field set.
//
// Supports:
// - changing types from atomic to granular
// - changing types from granular to atomic
func ReconcileFieldSetWithSchema(fieldset *fieldpath.Set, tv *TypedValue) (*fieldpath.Set, error) {
v := fmPool.Get().(*reconcileWithSchemaWalker)
v.fieldSet = fieldset
v.value = tv
v.schema = tv.schema
v.typeRef = tv.typeRef
defer v.finished()
errs := v.reconcile()
if len(errs) > 0 {
return nil, fmt.Errorf("errors reconciling field set with schema: %s", errs.Error())
}
// If there are any accumulated changes, apply them
if v.toAdd != nil || v.toRemove != nil {
out := v.fieldSet
if v.toRemove != nil {
out = out.RecursiveDifference(v.toRemove)
}
if v.toAdd != nil {
out = out.Union(v.toAdd)
}
return out, nil
}
return nil, nil
}
func (v *reconcileWithSchemaWalker) reconcile() (errs ValidationErrors) {
a, ok := v.schema.Resolve(v.typeRef)
if !ok {
errs = append(errs, errorf("could not resolve %v", v.typeRef)...)
return
}
return handleAtom(a, v.typeRef, v)
}
func (v *reconcileWithSchemaWalker) doScalar(_ *schema.Scalar) (errs ValidationErrors) {
return errs
}
func (v *reconcileWithSchemaWalker) visitListItems(t *schema.List, element *fieldpath.Set) (errs ValidationErrors) {
handleElement := func(pe fieldpath.PathElement, isMember bool) {
var hasChildren bool
v2 := v.prepareDescent(pe, t.ElementType)
v2.fieldSet, hasChildren = element.Children.Get(pe)
v2.isAtomic = isMember && !hasChildren
errs = append(errs, v2.reconcile()...)
v.finishDescent(v2)
}
element.Children.Iterate(func(pe fieldpath.PathElement) {
if element.Members.Has(pe) {
return
}
handleElement(pe, false)
})
element.Members.Iterate(func(pe fieldpath.PathElement) {
handleElement(pe, true)
})
return errs
}
func (v *reconcileWithSchemaWalker) doList(t *schema.List) (errs ValidationErrors) {
// reconcile lists changed from granular to atomic.
// Note that migrations from atomic to granular are not recommended and will
// be treated as if they were always granular.
//
// In this case, the manager that owned the previously atomic field (and all subfields),
// will now own just the top-level field and none of the subfields.
if !v.isAtomic && t.ElementRelationship == schema.Atomic {
v.toRemove = fieldpath.NewSet(v.path) // remove all root and all children fields
v.toAdd = fieldpath.NewSet(v.path) // add the root of the atomic
return errs
}
if v.fieldSet != nil {
errs = v.visitListItems(t, v.fieldSet)
}
return errs
}
func (v *reconcileWithSchemaWalker) visitMapItems(t *schema.Map, element *fieldpath.Set) (errs ValidationErrors) {
handleElement := func(pe fieldpath.PathElement, isMember bool) {
var hasChildren bool
if tr, ok := typeRefAtPath(t, pe); ok { // ignore fields not in the schema
v2 := v.prepareDescent(pe, tr)
v2.fieldSet, hasChildren = element.Children.Get(pe)
v2.isAtomic = isMember && !hasChildren
errs = append(errs, v2.reconcile()...)
v.finishDescent(v2)
}
}
element.Children.Iterate(func(pe fieldpath.PathElement) {
if element.Members.Has(pe) {
return
}
handleElement(pe, false)
})
element.Members.Iterate(func(pe fieldpath.PathElement) {
handleElement(pe, true)
})
return errs
}
func (v *reconcileWithSchemaWalker) doMap(t *schema.Map) (errs ValidationErrors) {
// We don't currently reconcile deduced types (unstructured CRDs) or maps that contain only unknown
// fields since deduced types do not yet support atomic or granular tags.
if isUntypedDeducedMap(t) {
return errs
}
// reconcile maps and structs changed from granular to atomic.
// Note that migrations from atomic to granular are not recommended and will
// be treated as if they were always granular.
//
// In this case the manager that owned the previously atomic field (and all subfields),
// will now own just the top-level field and none of the subfields.
if !v.isAtomic && t.ElementRelationship == schema.Atomic {
if v.fieldSet != nil && v.fieldSet.Size() > 0 {
v.toRemove = fieldpath.NewSet(v.path) // remove all root and all children fields
v.toAdd = fieldpath.NewSet(v.path) // add the root of the atomic
}
return errs
}
if v.fieldSet != nil {
errs = v.visitMapItems(t, v.fieldSet)
}
return errs
}
func fieldSetAtPath(node *fieldpath.Set, path fieldpath.Path) (*fieldpath.Set, bool) {
ok := true
for _, pe := range path {
if node, ok = node.Children.Get(pe); !ok {
break
}
}
return node, ok
}
func descendToPath(node *fieldpath.Set, path fieldpath.Path) *fieldpath.Set {
for _, pe := range path {
node = node.Children.Descend(pe)
}
return node
}
func typeRefAtPath(t *schema.Map, pe fieldpath.PathElement) (schema.TypeRef, bool) {
tr := t.ElementType
if pe.FieldName != nil {
if sf, ok := t.FindField(*pe.FieldName); ok {
tr = sf.Type
}
}
return tr, tr != schema.TypeRef{}
}
// isUntypedDeducedMap returns true if m has no fields defined, but allows untyped elements.
// This is equivalent to a openAPI object that has x-kubernetes-preserve-unknown-fields=true
// but does not have any properties defined on the object.
func isUntypedDeducedMap(m *schema.Map) bool {
return isUntypedDeducedRef(m.ElementType) && m.Fields == nil
}
func isUntypedDeducedRef(t schema.TypeRef) bool {
if t.NamedType != nil {
return *t.NamedType == "__untyped_deduced_"
}
atom := t.Inlined
return atom.Scalar != nil && *atom.Scalar == "untyped"
}
+204
View File
@@ -0,0 +1,204 @@
/*
Copyright 2019 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/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/schema"
"sigs.k8s.io/structured-merge-diff/v6/value"
)
type removingWalker struct {
value value.Value
out interface{}
schema *schema.Schema
toRemove *fieldpath.Set
allocator value.Allocator
shouldExtract bool
}
// removeItemsWithSchema will walk the given value and look for items from the toRemove set.
// Depending on whether shouldExtract is set true or false, it will return a modified version
// of the input value with either:
// 1. only the items in the toRemove set (when shouldExtract is true) or
// 2. the items from the toRemove set removed from the value (when shouldExtract is false).
func removeItemsWithSchema(val value.Value, toRemove *fieldpath.Set, schema *schema.Schema, typeRef schema.TypeRef, shouldExtract bool) value.Value {
w := &removingWalker{
value: val,
schema: schema,
toRemove: toRemove,
allocator: value.NewFreelistAllocator(),
shouldExtract: shouldExtract,
}
resolveSchema(schema, typeRef, val, w)
return value.NewValueInterface(w.out)
}
func (w *removingWalker) doScalar(t *schema.Scalar) ValidationErrors {
w.out = w.value.Unstructured()
return nil
}
func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) {
if !w.value.IsList() {
return nil
}
l := w.value.AsListUsing(w.allocator)
defer w.allocator.Free(l)
// If list is null or empty just return
if l == nil || l.Length() == 0 {
// For extraction, we just return the value as is (which is nil or empty). For extraction the difference matters.
if w.shouldExtract {
w.out = w.value.Unstructured()
}
return nil
}
// atomic lists should return everything in the case of extract
// and nothing in the case of remove (!w.shouldExtract)
if t.ElementRelationship == schema.Atomic {
if w.shouldExtract {
w.out = w.value.Unstructured()
}
return nil
}
var newItems []interface{}
hadMatches := false
iter := l.RangeUsing(w.allocator)
defer w.allocator.Free(iter)
for iter.Next() {
_, item := iter.Item()
// Ignore error because we have already validated this list
pe, _ := listItemToPathElement(w.allocator, w.schema, t, item)
path, _ := fieldpath.MakePath(pe)
// save items on the path when we shouldExtract
// but ignore them when we are removing (i.e. !w.shouldExtract)
isExactPathMatch := w.toRemove.Has(path)
isPrefixMatch := !w.toRemove.WithPrefix(pe).Empty()
if w.shouldExtract {
if isPrefixMatch {
item = removeItemsWithSchema(item, w.toRemove.WithPrefix(pe), w.schema, t.ElementType, w.shouldExtract)
}
if isExactPathMatch || isPrefixMatch {
newItems = append(newItems, item.Unstructured())
}
} else {
if isExactPathMatch {
continue
}
if isPrefixMatch {
// Removing nested items within this list item and preserve if it becomes empty
hadMatches = true
wasMap := item.IsMap()
wasList := item.IsList()
item = removeItemsWithSchema(item, w.toRemove.WithPrefix(pe), w.schema, t.ElementType, w.shouldExtract)
// If item returned null but we're removing items within the structure(not the item itself),
// preserve the empty container structure
if item.IsNull() && !w.shouldExtract {
if wasMap {
item = value.NewValueInterface(map[string]interface{}{})
} else if wasList {
item = value.NewValueInterface([]interface{}{})
}
}
}
newItems = append(newItems, item.Unstructured())
}
}
// Preserve empty lists (non-nil) instead of converting to null when items were matched and removed
if len(newItems) > 0 || (hadMatches && !w.shouldExtract) {
w.out = newItems
}
return nil
}
func (w *removingWalker) doMap(t *schema.Map) ValidationErrors {
if !w.value.IsMap() {
return nil
}
m := w.value.AsMapUsing(w.allocator)
if m != nil {
defer w.allocator.Free(m)
}
// If map is null or empty just return
if m == nil || m.Empty() {
// For extraction, we just return the value as is (which is nil or empty). For extraction the difference matters.
if w.shouldExtract {
w.out = w.value.Unstructured()
}
return nil
}
// atomic maps should return everything in the case of extract
// and nothing in the case of remove (!w.shouldExtract)
if t.ElementRelationship == schema.Atomic {
if w.shouldExtract {
w.out = w.value.Unstructured()
}
return nil
}
fieldTypes := map[string]schema.TypeRef{}
for _, structField := range t.Fields {
fieldTypes[structField.Name] = structField.Type
}
newMap := map[string]interface{}{}
hadMatches := false
m.Iterate(func(k string, val value.Value) bool {
pe := fieldpath.PathElement{FieldName: &k}
path, _ := fieldpath.MakePath(pe)
fieldType := t.ElementType
if ft, ok := fieldTypes[k]; ok {
fieldType = ft
}
// save values on the path when we shouldExtract
// but ignore them when we are removing (i.e. !w.shouldExtract)
if w.toRemove.Has(path) {
if w.shouldExtract {
newMap[k] = removeItemsWithSchema(val, w.toRemove, w.schema, fieldType, w.shouldExtract).Unstructured()
}
return true
}
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
hadMatches = true
wasMap := val.IsMap()
wasList := val.IsList()
val = removeItemsWithSchema(val, subset, w.schema, fieldType, w.shouldExtract)
// If val returned null but we're removing items within the structure (not the field itself),
// preserve the empty container structure
if val.IsNull() && !w.shouldExtract {
if wasMap {
val = value.NewValueInterface(map[string]interface{}{})
} else if wasList {
val = value.NewValueInterface([]interface{}{})
}
}
} else {
// don't save values not on the path when we shouldExtract.
if w.shouldExtract {
return true
}
}
newMap[k] = val.Unstructured()
return true
})
// Preserve empty maps (non-nil) instead of converting to null when items were matched and removed
if len(newMap) > 0 || (hadMatches && !w.shouldExtract) {
w.out = newMap
}
return nil
}
+190
View File
@@ -0,0 +1,190 @@
/*
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 (
"sync"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/schema"
"sigs.k8s.io/structured-merge-diff/v6/value"
)
var tPool = sync.Pool{
New: func() interface{} { return &toFieldSetWalker{} },
}
func (tv TypedValue) toFieldSetWalker() *toFieldSetWalker {
v := tPool.Get().(*toFieldSetWalker)
v.value = tv.value
v.schema = tv.schema
v.typeRef = tv.typeRef
v.set = &fieldpath.Set{}
v.allocator = value.NewFreelistAllocator()
return v
}
func (v *toFieldSetWalker) finished() {
v.schema = nil
v.typeRef = schema.TypeRef{}
v.path = nil
v.set = nil
tPool.Put(v)
}
type toFieldSetWalker struct {
value value.Value
schema *schema.Schema
typeRef schema.TypeRef
set *fieldpath.Set
path fieldpath.Path
// Allocate only as many walkers as needed for the depth by storing them here.
spareWalkers *[]*toFieldSetWalker
allocator value.Allocator
}
func (v *toFieldSetWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef) *toFieldSetWalker {
if v.spareWalkers == nil {
// first descent.
v.spareWalkers = &[]*toFieldSetWalker{}
}
var v2 *toFieldSetWalker
if n := len(*v.spareWalkers); n > 0 {
v2, *v.spareWalkers = (*v.spareWalkers)[n-1], (*v.spareWalkers)[:n-1]
} else {
v2 = &toFieldSetWalker{}
}
*v2 = *v
v2.typeRef = tr
v2.path = append(v2.path, pe)
return v2
}
func (v *toFieldSetWalker) finishDescent(v2 *toFieldSetWalker) {
// if the descent caused a realloc, ensure that we reuse the buffer
// for the next sibling.
v.path = v2.path[:len(v2.path)-1]
*v.spareWalkers = append(*v.spareWalkers, v2)
}
func (v *toFieldSetWalker) toFieldSet() ValidationErrors {
return resolveSchema(v.schema, v.typeRef, v.value, v)
}
func (v *toFieldSetWalker) doScalar(t *schema.Scalar) ValidationErrors {
v.set.Insert(v.path)
return nil
}
func (v *toFieldSetWalker) visitListItems(t *schema.List, list value.List) (errs ValidationErrors) {
// Keeps track of the PEs we've seen
seen := fieldpath.MakePathElementSet(list.Length())
// Keeps tracks of the PEs we've counted as duplicates
duplicates := fieldpath.MakePathElementSet(list.Length())
for i := 0; i < list.Length(); i++ {
child := list.At(i)
pe, _ := listItemToPathElement(v.allocator, v.schema, t, child)
if seen.Has(pe) {
if duplicates.Has(pe) {
// do nothing
} else {
v.set.Insert(append(v.path, pe))
duplicates.Insert(pe)
}
} else {
seen.Insert(pe)
}
}
for i := 0; i < list.Length(); i++ {
child := list.At(i)
pe, _ := listItemToPathElement(v.allocator, v.schema, t, child)
if duplicates.Has(pe) {
continue
}
v2 := v.prepareDescent(pe, t.ElementType)
v2.value = child
errs = append(errs, v2.toFieldSet()...)
v2.set.Insert(v2.path)
v.finishDescent(v2)
}
return errs
}
func (v *toFieldSetWalker) doList(t *schema.List) (errs ValidationErrors) {
list, _ := listValue(v.allocator, v.value)
if list != nil {
defer v.allocator.Free(list)
}
if t.ElementRelationship == schema.Atomic {
v.set.Insert(v.path)
return nil
}
if list == nil {
return nil
}
errs = v.visitListItems(t, list)
return errs
}
func (v *toFieldSetWalker) visitMapItems(t *schema.Map, m value.Map) (errs ValidationErrors) {
m.Iterate(func(key string, val value.Value) bool {
pe := fieldpath.PathElement{FieldName: &key}
tr := t.ElementType
if sf, ok := t.FindField(key); ok {
tr = sf.Type
}
v2 := v.prepareDescent(pe, tr)
v2.value = val
errs = append(errs, v2.toFieldSet()...)
if val.IsNull() || (val.IsMap() && val.AsMap().Length() == 0) {
v2.set.Insert(v2.path)
} else if _, ok := t.FindField(key); !ok {
v2.set.Insert(v2.path)
}
v.finishDescent(v2)
return true
})
return errs
}
func (v *toFieldSetWalker) doMap(t *schema.Map) (errs ValidationErrors) {
m, _ := mapValue(v.allocator, v.value)
if m != nil {
defer v.allocator.Free(m)
}
if t.ElementRelationship == schema.Atomic {
v.set.Insert(v.path)
return nil
}
if m == nil {
return nil
}
errs = v.visitMapItems(t, m)
return errs
}
+294
View File
@@ -0,0 +1,294 @@
/*
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 (
"sync"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/schema"
"sigs.k8s.io/structured-merge-diff/v6/value"
)
// ValidationOptions is the list of all the options available when running the validation.
type ValidationOptions int
const (
// AllowDuplicates means that sets and associative lists can have duplicate similar items.
AllowDuplicates ValidationOptions = iota
)
// extractItemsOptions is the options available when extracting items.
type extractItemsOptions struct {
appendKeyFields bool
}
type ExtractItemsOption func(*extractItemsOptions)
// WithAppendKeyFields configures ExtractItems to include key fields.
// It is exported for use in configuring ExtractItems.
func WithAppendKeyFields() ExtractItemsOption {
return func(opts *extractItemsOptions) {
opts.appendKeyFields = true
}
}
// 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, typeRef schema.TypeRef, opts ...ValidationOptions) (*TypedValue, error) {
tv := &TypedValue{
value: v,
typeRef: typeRef,
schema: s,
}
if err := tv.Validate(opts...); err != nil {
return nil, err
}
return tv, nil
}
// AsTypeUnvalidated is just like AsTyped, 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).
//
// Deprecated: This function was initially created because validation
// was expensive. Now that this has been solved, objects should always
// be created as validated, using `AsTyped`.
func AsTypedUnvalidated(v value.Value, s *schema.Schema, typeRef schema.TypeRef) *TypedValue {
tv := &TypedValue{
value: v,
typeRef: typeRef,
schema: s,
}
return tv
}
// TypedValue is a value of some specific type.
type TypedValue struct {
value value.Value
typeRef schema.TypeRef
schema *schema.Schema
}
// TypeRef is the type of the value.
func (tv TypedValue) TypeRef() schema.TypeRef {
return tv.typeRef
}
// AsValue removes the type from the TypedValue and only keeps the value.
func (tv TypedValue) AsValue() value.Value {
return tv.value
}
// Schema gets the schema from the TypedValue.
func (tv TypedValue) Schema() *schema.Schema {
return tv.schema
}
// Validate returns an error with a list of every spec violation.
func (tv TypedValue) Validate(opts ...ValidationOptions) error {
w := tv.walker()
for _, opt := range opts {
switch opt {
case AllowDuplicates:
w.allowDuplicates = true
}
}
defer w.finished()
if errs := w.validate(nil); len(errs) != 0 {
return errs
}
return nil
}
// ToFieldSet creates a set containing every leaf field and item mentioned, or
// validation errors, if any were encountered.
func (tv TypedValue) ToFieldSet() (*fieldpath.Set, error) {
w := tv.toFieldSetWalker()
defer w.finished()
if errs := w.toFieldSet(); len(errs) != 0 {
return nil, errs
}
return w.set, 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:
// 1. like tv, if pso doesn't change anything in the container
// 2. 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)
}
var cmpwPool = sync.Pool{
New: func() interface{} { return &compareWalker{} },
}
// 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) {
lhs := tv
if lhs.schema != rhs.schema {
return nil, errorf("expected objects with types from the same schema")
}
if !lhs.typeRef.Equals(&rhs.typeRef) {
return nil, errorf("expected objects of the same type, but got %v and %v", lhs.typeRef, rhs.typeRef)
}
cmpw := cmpwPool.Get().(*compareWalker)
defer func() {
cmpw.lhs = nil
cmpw.rhs = nil
cmpw.schema = nil
cmpw.typeRef = schema.TypeRef{}
cmpw.comparison = nil
cmpw.inLeaf = false
cmpwPool.Put(cmpw)
}()
cmpw.lhs = lhs.value
cmpw.rhs = rhs.value
cmpw.schema = lhs.schema
cmpw.typeRef = lhs.typeRef
cmpw.comparison = &Comparison{
Removed: fieldpath.NewSet(),
Modified: fieldpath.NewSet(),
Added: fieldpath.NewSet(),
}
if cmpw.allocator == nil {
cmpw.allocator = value.NewFreelistAllocator()
}
errs := cmpw.compare(nil)
if len(errs) > 0 {
return nil, errs
}
return cmpw.comparison, nil
}
// RemoveItems removes each provided list or map item from the value.
func (tv TypedValue) RemoveItems(items *fieldpath.Set) *TypedValue {
tv.value = removeItemsWithSchema(tv.value, items, tv.schema, tv.typeRef, false)
return &tv
}
// ExtractItems returns a value with only the provided list or map items extracted from the value.
func (tv TypedValue) ExtractItems(items *fieldpath.Set, opts ...ExtractItemsOption) *TypedValue {
options := &extractItemsOptions{}
for _, opt := range opts {
opt(options)
}
if options.appendKeyFields {
tvPathSet, err := tv.ToFieldSet()
if err == nil {
keyFieldPathSet := fieldpath.NewSet()
items.Iterate(func(path fieldpath.Path) {
if !tvPathSet.Has(path) {
return
}
for i, pe := range path {
if pe.Key == nil {
continue
}
for _, keyField := range *pe.Key {
keyName := keyField.Name
// Create a new slice with the same elements as path[:i+1], but set its capacity to len(path[:i+1]).
// This ensures that appending to keyFieldPath creates a new underlying array, avoiding accidental
// modification of the original slice (path).
keyFieldPath := append(path[:i+1:i+1], fieldpath.PathElement{FieldName: &keyName})
keyFieldPathSet.Insert(keyFieldPath)
}
}
})
items = items.Union(keyFieldPathSet)
}
}
tv.value = removeItemsWithSchema(tv.value, items, tv.schema, tv.typeRef, true)
return &tv
}
func (tv TypedValue) Empty() *TypedValue {
tv.value = value.NewValueInterface(nil)
return &tv
}
var mwPool = sync.Pool{
New: func() interface{} { return &mergingWalker{} },
}
func merge(lhs, rhs *TypedValue, rule, postRule mergeRule) (*TypedValue, error) {
if lhs.schema != rhs.schema {
return nil, errorf("expected objects with types from the same schema")
}
if !lhs.typeRef.Equals(&rhs.typeRef) {
return nil, errorf("expected objects of the same type, but got %v and %v", lhs.typeRef, rhs.typeRef)
}
mw := mwPool.Get().(*mergingWalker)
defer func() {
mw.lhs = nil
mw.rhs = nil
mw.schema = nil
mw.typeRef = schema.TypeRef{}
mw.rule = nil
mw.postItemHook = nil
mw.out = nil
mw.inLeaf = false
mwPool.Put(mw)
}()
mw.lhs = lhs.value
mw.rhs = rhs.value
mw.schema = lhs.schema
mw.typeRef = lhs.typeRef
mw.rule = rule
mw.postItemHook = postRule
if mw.allocator == nil {
mw.allocator = value.NewFreelistAllocator()
}
errs := mw.merge(nil)
if len(errs) > 0 {
return nil, errs
}
out := &TypedValue{
schema: lhs.schema,
typeRef: lhs.typeRef,
}
if mw.out != nil {
out.value = value.NewValueInterface(*mw.out)
}
return out, nil
}
+205
View File
@@ -0,0 +1,205 @@
/*
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 (
"sync"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
"sigs.k8s.io/structured-merge-diff/v6/schema"
"sigs.k8s.io/structured-merge-diff/v6/value"
)
var vPool = sync.Pool{
New: func() interface{} { return &validatingObjectWalker{} },
}
func (tv TypedValue) walker() *validatingObjectWalker {
v := vPool.Get().(*validatingObjectWalker)
v.value = tv.value
v.schema = tv.schema
v.typeRef = tv.typeRef
v.allowDuplicates = false
if v.allocator == nil {
v.allocator = value.NewFreelistAllocator()
}
return v
}
func (v *validatingObjectWalker) finished() {
v.schema = nil
v.typeRef = schema.TypeRef{}
vPool.Put(v)
}
type validatingObjectWalker struct {
value value.Value
schema *schema.Schema
typeRef schema.TypeRef
// If set to true, duplicates will be allowed in
// associativeLists/sets.
allowDuplicates bool
// Allocate only as many walkers as needed for the depth by storing them here.
spareWalkers *[]*validatingObjectWalker
allocator value.Allocator
}
func (v *validatingObjectWalker) prepareDescent(tr schema.TypeRef) *validatingObjectWalker {
if v.spareWalkers == nil {
// first descent.
v.spareWalkers = &[]*validatingObjectWalker{}
}
var v2 *validatingObjectWalker
if n := len(*v.spareWalkers); n > 0 {
v2, *v.spareWalkers = (*v.spareWalkers)[n-1], (*v.spareWalkers)[:n-1]
} else {
v2 = &validatingObjectWalker{}
}
*v2 = *v
v2.typeRef = tr
return v2
}
func (v *validatingObjectWalker) finishDescent(v2 *validatingObjectWalker) {
// if the descent caused a realloc, ensure that we reuse the buffer
// for the next sibling.
*v.spareWalkers = append(*v.spareWalkers, v2)
}
func (v *validatingObjectWalker) validate(prefixFn func() string) ValidationErrors {
return resolveSchema(v.schema, v.typeRef, v.value, v).WithLazyPrefix(prefixFn)
}
func validateScalar(t *schema.Scalar, v value.Value, prefix string) (errs ValidationErrors) {
if v == nil {
return nil
}
if v.IsNull() {
return nil
}
switch *t {
case schema.Numeric:
if !v.IsFloat() && !v.IsInt() {
// TODO: should the schema separate int and float?
return errorf("%vexpected numeric (int or float), got %T", prefix, v.Unstructured())
}
case schema.String:
if !v.IsString() {
return errorf("%vexpected string, got %#v", prefix, v)
}
case schema.Boolean:
if !v.IsBool() {
return errorf("%vexpected boolean, got %v", prefix, v)
}
case schema.Untyped:
if !v.IsFloat() && !v.IsInt() && !v.IsString() && !v.IsBool() {
return errorf("%vexpected any scalar, got %v", prefix, v)
}
default:
return errorf("%vunexpected scalar type in schema: %v", prefix, *t)
}
return nil
}
func (v *validatingObjectWalker) doScalar(t *schema.Scalar) ValidationErrors {
if errs := validateScalar(t, v.value, ""); len(errs) > 0 {
return errs
}
return nil
}
func (v *validatingObjectWalker) visitListItems(t *schema.List, list value.List) (errs ValidationErrors) {
observedKeys := fieldpath.MakePathElementSet(list.Length())
for i := 0; i < list.Length(); i++ {
child := list.AtUsing(v.allocator, i)
defer v.allocator.Free(child)
var pe fieldpath.PathElement
if t.ElementRelationship != schema.Associative {
pe.Index = &i
} else {
var err error
pe, err = listItemToPathElement(v.allocator, v.schema, t, child)
if err != nil {
errs = append(errs, 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.
return
}
if observedKeys.Has(pe) && !v.allowDuplicates {
errs = append(errs, errorf("duplicate entries for key %v", pe.String())...)
}
observedKeys.Insert(pe)
}
v2 := v.prepareDescent(t.ElementType)
v2.value = child
errs = append(errs, v2.validate(pe.String)...)
v.finishDescent(v2)
}
return errs
}
func (v *validatingObjectWalker) doList(t *schema.List) (errs ValidationErrors) {
list, err := listValue(v.allocator, v.value)
if err != nil {
return errorf("%v", err)
}
if list == nil {
return nil
}
defer v.allocator.Free(list)
errs = v.visitListItems(t, list)
return errs
}
func (v *validatingObjectWalker) visitMapItems(t *schema.Map, m value.Map) (errs ValidationErrors) {
m.IterateUsing(v.allocator, func(key string, val value.Value) bool {
pe := fieldpath.PathElement{FieldName: &key}
tr := t.ElementType
if sf, ok := t.FindField(key); ok {
tr = sf.Type
} else if (t.ElementType == schema.TypeRef{}) {
errs = append(errs, errorf("field not declared in schema").WithPrefix(pe.String())...)
return false
}
v2 := v.prepareDescent(tr)
v2.value = val
// Giving pe.String as a parameter actually increases the allocations.
errs = append(errs, v2.validate(func() string { return pe.String() })...)
v.finishDescent(v2)
return true
})
return errs
}
func (v *validatingObjectWalker) doMap(t *schema.Map) (errs ValidationErrors) {
m, err := mapValue(v.allocator, v.value)
if err != nil {
return errorf("%v", err)
}
if m == nil {
return nil
}
defer v.allocator.Free(m)
errs = v.visitMapItems(t, m)
return errs
}
+203
View File
@@ -0,0 +1,203 @@
/*
Copyright 2020 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
// Allocator provides a value object allocation strategy.
// Value objects can be allocated by passing an allocator to the "Using"
// receiver functions on the value interfaces, e.g. Map.ZipUsing(allocator, ...).
// Value objects returned from "Using" functions should be given back to the allocator
// once longer needed by calling Allocator.Free(Value).
type Allocator interface {
// Free gives the allocator back any value objects returned by the "Using"
// receiver functions on the value interfaces.
// interface{} may be any of: Value, Map, List or Range.
Free(interface{})
// The unexported functions are for "Using" receiver functions of the value types
// to request what they need from the allocator.
allocValueUnstructured() *valueUnstructured
allocListUnstructuredRange() *listUnstructuredRange
allocValueReflect() *valueReflect
allocMapReflect() *mapReflect
allocStructReflect() *structReflect
allocListReflect() *listReflect
allocListReflectRange() *listReflectRange
}
// HeapAllocator simply allocates objects to the heap. It is the default
// allocator used receiver functions on the value interfaces that do not accept
// an allocator and should be used whenever allocating objects that will not
// be given back to an allocator by calling Allocator.Free(Value).
var HeapAllocator = &heapAllocator{}
type heapAllocator struct{}
func (p *heapAllocator) allocValueUnstructured() *valueUnstructured {
return &valueUnstructured{}
}
func (p *heapAllocator) allocListUnstructuredRange() *listUnstructuredRange {
return &listUnstructuredRange{vv: &valueUnstructured{}}
}
func (p *heapAllocator) allocValueReflect() *valueReflect {
return &valueReflect{}
}
func (p *heapAllocator) allocStructReflect() *structReflect {
return &structReflect{}
}
func (p *heapAllocator) allocMapReflect() *mapReflect {
return &mapReflect{}
}
func (p *heapAllocator) allocListReflect() *listReflect {
return &listReflect{}
}
func (p *heapAllocator) allocListReflectRange() *listReflectRange {
return &listReflectRange{vr: &valueReflect{}}
}
func (p *heapAllocator) Free(_ interface{}) {}
// NewFreelistAllocator creates freelist based allocator.
// This allocator provides fast allocation and freeing of short lived value objects.
//
// The freelists are bounded in size by freelistMaxSize. If more than this amount of value objects is
// allocated at once, the excess will be returned to the heap for garbage collection when freed.
//
// This allocator is unsafe and must not be accessed concurrently by goroutines.
//
// This allocator works well for traversal of value data trees. Typical usage is to acquire
// a freelist at the beginning of the traversal and use it through out
// for all temporary value access.
func NewFreelistAllocator() Allocator {
return &freelistAllocator{
valueUnstructured: &freelist{new: func() interface{} {
return &valueUnstructured{}
}},
listUnstructuredRange: &freelist{new: func() interface{} {
return &listUnstructuredRange{vv: &valueUnstructured{}}
}},
valueReflect: &freelist{new: func() interface{} {
return &valueReflect{}
}},
mapReflect: &freelist{new: func() interface{} {
return &mapReflect{}
}},
structReflect: &freelist{new: func() interface{} {
return &structReflect{}
}},
listReflect: &freelist{new: func() interface{} {
return &listReflect{}
}},
listReflectRange: &freelist{new: func() interface{} {
return &listReflectRange{vr: &valueReflect{}}
}},
}
}
// Bound memory usage of freelists. This prevents the processing of very large lists from leaking memory.
// This limit is large enough for endpoints objects containing 1000 IP address entries. Freed objects
// that don't fit into the freelist are orphaned on the heap to be garbage collected.
const freelistMaxSize = 1000
type freelistAllocator struct {
valueUnstructured *freelist
listUnstructuredRange *freelist
valueReflect *freelist
mapReflect *freelist
structReflect *freelist
listReflect *freelist
listReflectRange *freelist
}
type freelist struct {
list []interface{}
new func() interface{}
}
func (f *freelist) allocate() interface{} {
var w2 interface{}
if n := len(f.list); n > 0 {
w2, f.list = f.list[n-1], f.list[:n-1]
} else {
w2 = f.new()
}
return w2
}
func (f *freelist) free(v interface{}) {
if len(f.list) < freelistMaxSize {
f.list = append(f.list, v)
}
}
func (w *freelistAllocator) Free(value interface{}) {
switch v := value.(type) {
case *valueUnstructured:
v.Value = nil // don't hold references to unstructured objects
w.valueUnstructured.free(v)
case *listUnstructuredRange:
v.vv.Value = nil // don't hold references to unstructured objects
w.listUnstructuredRange.free(v)
case *valueReflect:
v.ParentMapKey = nil
v.ParentMap = nil
w.valueReflect.free(v)
case *mapReflect:
w.mapReflect.free(v)
case *structReflect:
w.structReflect.free(v)
case *listReflect:
w.listReflect.free(v)
case *listReflectRange:
v.vr.ParentMapKey = nil
v.vr.ParentMap = nil
w.listReflectRange.free(v)
}
}
func (w *freelistAllocator) allocValueUnstructured() *valueUnstructured {
return w.valueUnstructured.allocate().(*valueUnstructured)
}
func (w *freelistAllocator) allocListUnstructuredRange() *listUnstructuredRange {
return w.listUnstructuredRange.allocate().(*listUnstructuredRange)
}
func (w *freelistAllocator) allocValueReflect() *valueReflect {
return w.valueReflect.allocate().(*valueReflect)
}
func (w *freelistAllocator) allocStructReflect() *structReflect {
return w.structReflect.allocate().(*structReflect)
}
func (w *freelistAllocator) allocMapReflect() *mapReflect {
return w.mapReflect.allocate().(*mapReflect)
}
func (w *freelistAllocator) allocListReflect() *listReflect {
return w.listReflect.allocate().(*listReflect)
}
func (w *freelistAllocator) allocListReflectRange() *listReflectRange {
return w.listReflectRange.allocate().(*listReflectRange)
}
+21
View File
@@ -0,0 +1,21 @@
/*
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
+105
View File
@@ -0,0 +1,105 @@
/*
Copyright 2019 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 (
"sort"
"strings"
)
// Field is an individual key-value pair.
type Field struct {
Name string
Value Value
}
// FieldList is a list of key-value pairs. Each field is expected to
// have a different name.
type FieldList []Field
// Copy returns a copy of the FieldList.
// Values are not copied.
func (f FieldList) Copy() FieldList {
c := make(FieldList, len(f))
copy(c, f)
return c
}
// Sort sorts the field list by Name.
func (f FieldList) Sort() {
if len(f) < 2 {
return
}
if len(f) == 2 {
if f[1].Name < f[0].Name {
f[0], f[1] = f[1], f[0]
}
return
}
sort.SliceStable(f, func(i, j int) bool {
return f[i].Name < f[j].Name
})
}
// Less compares two lists lexically.
func (f FieldList) Less(rhs FieldList) bool {
return f.Compare(rhs) == -1
}
// Compare compares two lists lexically. The result will be 0 if f==rhs, -1
// if f < rhs, and +1 if f > rhs.
func (f FieldList) Compare(rhs FieldList) int {
i := 0
for {
if i >= len(f) && i >= len(rhs) {
// Maps are the same length and all items are equal.
return 0
}
if i >= len(f) {
// F is shorter.
return -1
}
if i >= len(rhs) {
// RHS is shorter.
return 1
}
if c := strings.Compare(f[i].Name, rhs[i].Name); c != 0 {
return c
}
if c := Compare(f[i].Value, rhs[i].Value); c != 0 {
return c
}
// The items are equal; continue.
i++
}
}
// Equals returns true if the two fieldslist are equals, false otherwise.
func (f FieldList) Equals(rhs FieldList) bool {
if len(f) != len(rhs) {
return false
}
for i := range f {
if f[i].Name != rhs[i].Name {
return false
}
if !Equals(f[i].Value, rhs[i].Value) {
return false
}
}
return true
}
+146
View File
@@ -0,0 +1,146 @@
/*
Copyright 2019 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"
"reflect"
"strings"
)
type isZeroer interface {
IsZero() bool
}
var isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem()
func reflectIsZero(dv reflect.Value) bool {
return dv.IsZero()
}
// OmitZeroFunc returns a function for a type for a given struct field
// which determines if the value for that field is a zero value, matching
// how the stdlib JSON implementation.
func OmitZeroFunc(t reflect.Type) func(reflect.Value) bool {
// Provide a function that uses a type's IsZero method.
// This matches the go 1.24 custom IsZero() implementation matching
switch {
case t.Kind() == reflect.Interface && t.Implements(isZeroerType):
return func(v reflect.Value) bool {
// Avoid panics calling IsZero on a nil interface or
// non-nil interface with nil pointer.
return safeIsNil(v) ||
(v.Elem().Kind() == reflect.Pointer && v.Elem().IsNil()) ||
v.Interface().(isZeroer).IsZero()
}
case t.Kind() == reflect.Pointer && t.Implements(isZeroerType):
return func(v reflect.Value) bool {
// Avoid panics calling IsZero on nil pointer.
return safeIsNil(v) || v.Interface().(isZeroer).IsZero()
}
case t.Implements(isZeroerType):
return func(v reflect.Value) bool {
return v.Interface().(isZeroer).IsZero()
}
case reflect.PointerTo(t).Implements(isZeroerType):
return func(v reflect.Value) bool {
if !v.CanAddr() {
// Temporarily box v so we can take the address.
v2 := reflect.New(v.Type()).Elem()
v2.Set(v)
v = v2
}
return v.Addr().Interface().(isZeroer).IsZero()
}
default:
// default to the reflect.IsZero implementation
return reflectIsZero
}
}
// TODO: This implements the same functionality as https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go#L236
// but is based on the highly efficient approach from https://golang.org/src/encoding/json/encode.go
func lookupJsonTags(f reflect.StructField) (name string, omit bool, inline bool, omitempty bool, omitzero func(reflect.Value) bool) {
tag := f.Tag.Get("json")
if tag == "-" {
return "", true, false, false, nil
}
name, opts := parseTag(tag)
if name == "" {
name = f.Name
}
if opts.Contains("omitzero") {
omitzero = OmitZeroFunc(f.Type)
}
return name, false, opts.Contains("inline"), opts.Contains("omitempty"), omitzero
}
func isEmpty(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
case reflect.Chan, reflect.Func:
panic(fmt.Sprintf("unsupported type: %v", v.Type()))
}
return false
}
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
}
+139
View File
@@ -0,0 +1,139 @@
/*
Copyright 2019 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
// List represents a list object.
type List interface {
// Length returns how many items can be found in the map.
Length() int
// At returns the item at the given position in the map. It will
// panic if the index is out of range.
At(int) Value
// AtUsing uses the provided allocator and returns the item at the given
// position in the map. It will panic if the index is out of range.
// The returned Value should be given back to the Allocator when no longer needed
// by calling Allocator.Free(Value).
AtUsing(Allocator, int) Value
// Range returns a ListRange for iterating over the items in the list.
Range() ListRange
// RangeUsing uses the provided allocator and returns a ListRange for
// iterating over the items in the list.
// The returned Range should be given back to the Allocator when no longer needed
// by calling Allocator.Free(Value).
RangeUsing(Allocator) ListRange
// Equals compares the two lists, and return true if they are the same, false otherwise.
// Implementations can use ListEquals as a general implementation for this methods.
Equals(List) bool
// EqualsUsing uses the provided allocator and compares the two lists, and return true if
// they are the same, false otherwise. Implementations can use ListEqualsUsing as a general
// implementation for this methods.
EqualsUsing(Allocator, List) bool
}
// ListRange represents a single iteration across the items of a list.
type ListRange interface {
// Next increments to the next item in the range, if there is one, and returns true, or returns false if there are no more items.
Next() bool
// Item returns the index and value of the current item in the range. or panics if there is no current item.
// For efficiency, Item may reuse the values returned by previous Item calls. Callers should be careful avoid holding
// pointers to the value returned by Item() that escape the iteration loop since they become invalid once either
// Item() or Allocator.Free() is called.
Item() (index int, value Value)
}
var EmptyRange = &emptyRange{}
type emptyRange struct{}
func (_ *emptyRange) Next() bool {
return false
}
func (_ *emptyRange) Item() (index int, value Value) {
panic("Item called on empty ListRange")
}
// ListEquals compares two lists lexically.
// WARN: This is a naive implementation, calling lhs.Equals(rhs) is typically the most efficient.
func ListEquals(lhs, rhs List) bool {
return ListEqualsUsing(HeapAllocator, lhs, rhs)
}
// ListEqualsUsing uses the provided allocator and compares two lists lexically.
// WARN: This is a naive implementation, calling lhs.EqualsUsing(allocator, rhs) is typically the most efficient.
func ListEqualsUsing(a Allocator, lhs, rhs List) bool {
if lhs.Length() != rhs.Length() {
return false
}
lhsRange := lhs.RangeUsing(a)
defer a.Free(lhsRange)
rhsRange := rhs.RangeUsing(a)
defer a.Free(rhsRange)
for lhsRange.Next() && rhsRange.Next() {
_, lv := lhsRange.Item()
_, rv := rhsRange.Item()
if !EqualsUsing(a, lv, rv) {
return false
}
}
return true
}
// ListLess compares two lists lexically.
func ListLess(lhs, rhs List) bool {
return ListCompare(lhs, rhs) == -1
}
// ListCompare compares two lists lexically. The result will be 0 if l==rhs, -1
// if l < rhs, and +1 if l > rhs.
func ListCompare(lhs, rhs List) int {
return ListCompareUsing(HeapAllocator, lhs, rhs)
}
// ListCompareUsing uses the provided allocator and compares two lists lexically. The result will be 0 if l==rhs, -1
// if l < rhs, and +1 if l > rhs.
func ListCompareUsing(a Allocator, lhs, rhs List) int {
lhsRange := lhs.RangeUsing(a)
defer a.Free(lhsRange)
rhsRange := rhs.RangeUsing(a)
defer a.Free(rhsRange)
for {
lhsOk := lhsRange.Next()
rhsOk := rhsRange.Next()
if !lhsOk && !rhsOk {
// Lists are the same length and all items are equal.
return 0
}
if !lhsOk {
// LHS is shorter.
return -1
}
if !rhsOk {
// RHS is shorter.
return 1
}
_, lv := lhsRange.Item()
_, rv := rhsRange.Item()
if c := CompareUsing(a, lv, rv); c != 0 {
return c
}
// The items are equal; continue.
}
}
+98
View File
@@ -0,0 +1,98 @@
/*
Copyright 2019 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 (
"reflect"
)
type listReflect struct {
Value reflect.Value
}
func (r listReflect) Length() int {
val := r.Value
return val.Len()
}
func (r listReflect) At(i int) Value {
val := r.Value
return mustWrapValueReflect(val.Index(i), nil, nil)
}
func (r listReflect) AtUsing(a Allocator, i int) Value {
val := r.Value
return a.allocValueReflect().mustReuse(val.Index(i), nil, nil, nil)
}
func (r listReflect) Unstructured() interface{} {
l := r.Length()
result := make([]interface{}, l)
for i := 0; i < l; i++ {
result[i] = r.At(i).Unstructured()
}
return result
}
func (r listReflect) Range() ListRange {
return r.RangeUsing(HeapAllocator)
}
func (r listReflect) RangeUsing(a Allocator) ListRange {
length := r.Value.Len()
if length == 0 {
return EmptyRange
}
rr := a.allocListReflectRange()
rr.list = r.Value
rr.i = -1
rr.entry = TypeReflectEntryOf(r.Value.Type().Elem())
return rr
}
func (r listReflect) Equals(other List) bool {
return r.EqualsUsing(HeapAllocator, other)
}
func (r listReflect) EqualsUsing(a Allocator, other List) bool {
if otherReflectList, ok := other.(*listReflect); ok {
return reflect.DeepEqual(r.Value.Interface(), otherReflectList.Value.Interface())
}
return ListEqualsUsing(a, &r, other)
}
type listReflectRange struct {
list reflect.Value
vr *valueReflect
i int
entry *TypeReflectCacheEntry
}
func (r *listReflectRange) Next() bool {
r.i += 1
return r.i < r.list.Len()
}
func (r *listReflectRange) Item() (index int, value Value) {
if r.i < 0 {
panic("Item() called before first calling Next()")
}
if r.i >= r.list.Len() {
panic("Item() called on ListRange with no more items")
}
v := r.list.Index(r.i)
return r.i, r.vr.mustReuse(v, r.entry, nil, nil)
}
+74
View File
@@ -0,0 +1,74 @@
/*
Copyright 2019 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
type listUnstructured []interface{}
func (l listUnstructured) Length() int {
return len(l)
}
func (l listUnstructured) At(i int) Value {
return NewValueInterface(l[i])
}
func (l listUnstructured) AtUsing(a Allocator, i int) Value {
return a.allocValueUnstructured().reuse(l[i])
}
func (l listUnstructured) Equals(other List) bool {
return l.EqualsUsing(HeapAllocator, other)
}
func (l listUnstructured) EqualsUsing(a Allocator, other List) bool {
return ListEqualsUsing(a, &l, other)
}
func (l listUnstructured) Range() ListRange {
return l.RangeUsing(HeapAllocator)
}
func (l listUnstructured) RangeUsing(a Allocator) ListRange {
if len(l) == 0 {
return EmptyRange
}
r := a.allocListUnstructuredRange()
r.list = l
r.i = -1
return r
}
type listUnstructuredRange struct {
list listUnstructured
vv *valueUnstructured
i int
}
func (r *listUnstructuredRange) Next() bool {
r.i += 1
return r.i < len(r.list)
}
func (r *listUnstructuredRange) Item() (index int, value Value) {
if r.i < 0 {
panic("Item() called before first calling Next()")
}
if r.i >= len(r.list) {
panic("Item() called on ListRange with no more items")
}
return r.i, r.vv.reuse(r.list[r.i])
}
+270
View File
@@ -0,0 +1,270 @@
/*
Copyright 2019 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 (
"sort"
)
// Map represents a Map or go structure.
type Map interface {
// Set changes or set the value of the given key.
Set(key string, val Value)
// Get returns the value for the given key, if present, or (nil, false) otherwise.
Get(key string) (Value, bool)
// GetUsing uses the provided allocator and returns the value for the given key,
// if present, or (nil, false) otherwise.
// The returned Value should be given back to the Allocator when no longer needed
// by calling Allocator.Free(Value).
GetUsing(a Allocator, key string) (Value, bool)
// Has returns true if the key is present, or false otherwise.
Has(key string) bool
// Delete removes the key from the map.
Delete(key string)
// Equals compares the two maps, and return true if they are the same, false otherwise.
// Implementations can use MapEquals as a general implementation for this methods.
Equals(other Map) bool
// EqualsUsing uses the provided allocator and compares the two maps, and return true if
// they are the same, false otherwise. Implementations can use MapEqualsUsing as a general
// implementation for this methods.
EqualsUsing(a Allocator, other Map) bool
// Iterate runs the given function for each key/value in the
// map. Returning false in the closure prematurely stops the
// iteration.
Iterate(func(key string, value Value) bool) bool
// IterateUsing uses the provided allocator and runs the given function for each key/value
// in the map. Returning false in the closure prematurely stops the iteration.
IterateUsing(Allocator, func(key string, value Value) bool) bool
// Length returns the number of items in the map.
Length() int
// Empty returns true if the map is empty.
Empty() bool
// Zip iterates over the entries of two maps together. If both maps contain a value for a given key, fn is called
// with the values from both maps, otherwise it is called with the value of the map that contains the key and nil
// for the map that does not contain the key. Returning false in the closure prematurely stops the iteration.
Zip(other Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool
// ZipUsing uses the provided allocator and iterates over the entries of two maps together. If both maps
// contain a value for a given key, fn is called with the values from both maps, otherwise it is called with
// the value of the map that contains the key and nil for the map that does not contain the key. Returning
// false in the closure prematurely stops the iteration.
ZipUsing(a Allocator, other Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool
}
// MapTraverseOrder defines the map traversal ordering available.
type MapTraverseOrder int
const (
// Unordered indicates that the map traversal has no ordering requirement.
Unordered = iota
// LexicalKeyOrder indicates that the map traversal is ordered by key, lexically.
LexicalKeyOrder
)
// MapZip iterates over the entries of two maps together. If both maps contain a value for a given key, fn is called
// with the values from both maps, otherwise it is called with the value of the map that contains the key and nil
// for the other map. Returning false in the closure prematurely stops the iteration.
func MapZip(lhs, rhs Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
return MapZipUsing(HeapAllocator, lhs, rhs, order, fn)
}
// MapZipUsing uses the provided allocator and iterates over the entries of two maps together. If both maps
// contain a value for a given key, fn is called with the values from both maps, otherwise it is called with
// the value of the map that contains the key and nil for the other map. Returning false in the closure
// prematurely stops the iteration.
func MapZipUsing(a Allocator, lhs, rhs Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
if lhs != nil {
return lhs.ZipUsing(a, rhs, order, fn)
}
if rhs != nil {
return rhs.ZipUsing(a, lhs, order, func(key string, rhs, lhs Value) bool { // arg positions of lhs and rhs deliberately swapped
return fn(key, lhs, rhs)
})
}
return true
}
// defaultMapZip provides a default implementation of Zip for implementations that do not need to provide
// their own optimized implementation.
func defaultMapZip(a Allocator, lhs, rhs Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
switch order {
case Unordered:
return unorderedMapZip(a, lhs, rhs, fn)
case LexicalKeyOrder:
return lexicalKeyOrderedMapZip(a, lhs, rhs, fn)
default:
panic("Unsupported map order")
}
}
func unorderedMapZip(a Allocator, lhs, rhs Map, fn func(key string, lhs, rhs Value) bool) bool {
if (lhs == nil || lhs.Empty()) && (rhs == nil || rhs.Empty()) {
return true
}
if lhs != nil {
ok := lhs.IterateUsing(a, func(key string, lhsValue Value) bool {
var rhsValue Value
if rhs != nil {
if item, ok := rhs.GetUsing(a, key); ok {
rhsValue = item
defer a.Free(rhsValue)
}
}
return fn(key, lhsValue, rhsValue)
})
if !ok {
return false
}
}
if rhs != nil {
return rhs.IterateUsing(a, func(key string, rhsValue Value) bool {
if lhs == nil || !lhs.Has(key) {
return fn(key, nil, rhsValue)
}
return true
})
}
return true
}
func lexicalKeyOrderedMapZip(a Allocator, lhs, rhs Map, fn func(key string, lhs, rhs Value) bool) bool {
var lhsLength, rhsLength int
var orderedLength int // rough estimate of length of union of map keys
if lhs != nil {
lhsLength = lhs.Length()
orderedLength = lhsLength
}
if rhs != nil {
rhsLength = rhs.Length()
if rhsLength > orderedLength {
orderedLength = rhsLength
}
}
if lhsLength == 0 && rhsLength == 0 {
return true
}
ordered := make([]string, 0, orderedLength)
if lhs != nil {
lhs.IterateUsing(a, func(key string, _ Value) bool {
ordered = append(ordered, key)
return true
})
}
if rhs != nil {
rhs.IterateUsing(a, func(key string, _ Value) bool {
if lhs == nil || !lhs.Has(key) {
ordered = append(ordered, key)
}
return true
})
}
sort.Strings(ordered)
for _, key := range ordered {
var litem, ritem Value
if lhs != nil {
litem, _ = lhs.GetUsing(a, key)
}
if rhs != nil {
ritem, _ = rhs.GetUsing(a, key)
}
ok := fn(key, litem, ritem)
if litem != nil {
a.Free(litem)
}
if ritem != nil {
a.Free(ritem)
}
if !ok {
return false
}
}
return true
}
// MapLess compares two maps lexically.
func MapLess(lhs, rhs Map) bool {
return MapCompare(lhs, rhs) == -1
}
// MapCompare compares two maps lexically.
func MapCompare(lhs, rhs Map) int {
return MapCompareUsing(HeapAllocator, lhs, rhs)
}
// MapCompareUsing uses the provided allocator and compares two maps lexically.
func MapCompareUsing(a Allocator, lhs, rhs Map) int {
c := 0
var llength, rlength int
if lhs != nil {
llength = lhs.Length()
}
if rhs != nil {
rlength = rhs.Length()
}
if llength == 0 && rlength == 0 {
return 0
}
i := 0
MapZipUsing(a, lhs, rhs, LexicalKeyOrder, func(key string, lhs, rhs Value) bool {
switch {
case i == llength:
c = -1
case i == rlength:
c = 1
case lhs == nil:
c = 1
case rhs == nil:
c = -1
default:
c = CompareUsing(a, lhs, rhs)
}
i++
return c == 0
})
return c
}
// MapEquals returns true if lhs == rhs, false otherwise. This function
// acts on generic types and should not be used by callers, but can help
// implement Map.Equals.
// WARN: This is a naive implementation, calling lhs.Equals(rhs) is typically the most efficient.
func MapEquals(lhs, rhs Map) bool {
return MapEqualsUsing(HeapAllocator, lhs, rhs)
}
// MapEqualsUsing uses the provided allocator and returns true if lhs == rhs,
// false otherwise. This function acts on generic types and should not be used
// by callers, but can help implement Map.Equals.
// WARN: This is a naive implementation, calling lhs.EqualsUsing(allocator, rhs) is typically the most efficient.
func MapEqualsUsing(a Allocator, lhs, rhs Map) bool {
if lhs == nil && rhs == nil {
return true
}
if lhs == nil || rhs == nil {
return false
}
if lhs.Length() != rhs.Length() {
return false
}
return MapZipUsing(a, lhs, rhs, Unordered, func(key string, lhs, rhs Value) bool {
if lhs == nil || rhs == nil {
return false
}
return EqualsUsing(a, lhs, rhs)
})
}
+209
View File
@@ -0,0 +1,209 @@
/*
Copyright 2019 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 (
"reflect"
)
type mapReflect struct {
valueReflect
}
func (r mapReflect) Length() int {
val := r.Value
return val.Len()
}
func (r mapReflect) Empty() bool {
val := r.Value
return val.Len() == 0
}
func (r mapReflect) Get(key string) (Value, bool) {
return r.GetUsing(HeapAllocator, key)
}
func (r mapReflect) GetUsing(a Allocator, key string) (Value, bool) {
k, v, ok := r.get(key)
if !ok {
return nil, false
}
return a.allocValueReflect().mustReuse(v, nil, &r.Value, &k), true
}
func (r mapReflect) get(k string) (key, value reflect.Value, ok bool) {
mapKey := r.toMapKey(k)
val := r.Value.MapIndex(mapKey)
return mapKey, val, val.IsValid() && val != reflect.Value{}
}
func (r mapReflect) Has(key string) bool {
var val reflect.Value
val = r.Value.MapIndex(r.toMapKey(key))
if !val.IsValid() {
return false
}
return val != reflect.Value{}
}
func (r mapReflect) Set(key string, val Value) {
r.Value.SetMapIndex(r.toMapKey(key), reflect.ValueOf(val.Unstructured()))
}
func (r mapReflect) Delete(key string) {
val := r.Value
val.SetMapIndex(r.toMapKey(key), reflect.Value{})
}
// TODO: Do we need to support types that implement json.Marshaler and are used as string keys?
func (r mapReflect) toMapKey(key string) reflect.Value {
val := r.Value
return reflect.ValueOf(key).Convert(val.Type().Key())
}
func (r mapReflect) Iterate(fn func(string, Value) bool) bool {
return r.IterateUsing(HeapAllocator, fn)
}
func (r mapReflect) IterateUsing(a Allocator, fn func(string, Value) bool) bool {
if r.Value.Len() == 0 {
return true
}
v := a.allocValueReflect()
defer a.Free(v)
return eachMapEntry(r.Value, func(e *TypeReflectCacheEntry, key reflect.Value, value reflect.Value) bool {
return fn(key.String(), v.mustReuse(value, e, &r.Value, &key))
})
}
func eachMapEntry(val reflect.Value, fn func(*TypeReflectCacheEntry, reflect.Value, reflect.Value) bool) bool {
iter := val.MapRange()
entry := TypeReflectEntryOf(val.Type().Elem())
for iter.Next() {
next := iter.Value()
if !next.IsValid() {
continue
}
if !fn(entry, iter.Key(), next) {
return false
}
}
return true
}
func (r mapReflect) Unstructured() interface{} {
result := make(map[string]interface{}, r.Length())
r.Iterate(func(s string, value Value) bool {
result[s] = value.Unstructured()
return true
})
return result
}
func (r mapReflect) Equals(m Map) bool {
return r.EqualsUsing(HeapAllocator, m)
}
func (r mapReflect) EqualsUsing(a Allocator, m Map) bool {
lhsLength := r.Length()
rhsLength := m.Length()
if lhsLength != rhsLength {
return false
}
if lhsLength == 0 {
return true
}
vr := a.allocValueReflect()
defer a.Free(vr)
entry := TypeReflectEntryOf(r.Value.Type().Elem())
return m.Iterate(func(key string, value Value) bool {
_, lhsVal, ok := r.get(key)
if !ok {
return false
}
return EqualsUsing(a, vr.mustReuse(lhsVal, entry, nil, nil), value)
})
}
func (r mapReflect) Zip(other Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
return r.ZipUsing(HeapAllocator, other, order, fn)
}
func (r mapReflect) ZipUsing(a Allocator, other Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
if otherMapReflect, ok := other.(*mapReflect); ok && order == Unordered {
return r.unorderedReflectZip(a, otherMapReflect, fn)
}
return defaultMapZip(a, &r, other, order, fn)
}
// unorderedReflectZip provides an optimized unordered zip for mapReflect types.
func (r mapReflect) unorderedReflectZip(a Allocator, other *mapReflect, fn func(key string, lhs, rhs Value) bool) bool {
if r.Empty() && (other == nil || other.Empty()) {
return true
}
lhs := r.Value
lhsEntry := TypeReflectEntryOf(lhs.Type().Elem())
// map lookup via reflection is expensive enough that it is better to keep track of visited keys
visited := map[string]struct{}{}
vlhs, vrhs := a.allocValueReflect(), a.allocValueReflect()
defer a.Free(vlhs)
defer a.Free(vrhs)
if other != nil {
rhs := other.Value
rhsEntry := TypeReflectEntryOf(rhs.Type().Elem())
iter := rhs.MapRange()
for iter.Next() {
key := iter.Key()
keyString := key.String()
next := iter.Value()
if !next.IsValid() {
continue
}
rhsVal := vrhs.mustReuse(next, rhsEntry, &rhs, &key)
visited[keyString] = struct{}{}
var lhsVal Value
if _, v, ok := r.get(keyString); ok {
lhsVal = vlhs.mustReuse(v, lhsEntry, &lhs, &key)
}
if !fn(keyString, lhsVal, rhsVal) {
return false
}
}
}
iter := lhs.MapRange()
for iter.Next() {
key := iter.Key()
if _, ok := visited[key.String()]; ok {
continue
}
next := iter.Value()
if !next.IsValid() {
continue
}
if !fn(key.String(), vlhs.mustReuse(next, lhsEntry, &lhs, &key), nil) {
return false
}
}
return true
}
+190
View File
@@ -0,0 +1,190 @@
/*
Copyright 2019 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
type mapUnstructuredInterface map[interface{}]interface{}
func (m mapUnstructuredInterface) Set(key string, val Value) {
m[key] = val.Unstructured()
}
func (m mapUnstructuredInterface) Get(key string) (Value, bool) {
return m.GetUsing(HeapAllocator, key)
}
func (m mapUnstructuredInterface) GetUsing(a Allocator, key string) (Value, bool) {
if v, ok := m[key]; !ok {
return nil, false
} else {
return a.allocValueUnstructured().reuse(v), true
}
}
func (m mapUnstructuredInterface) Has(key string) bool {
_, ok := m[key]
return ok
}
func (m mapUnstructuredInterface) Delete(key string) {
delete(m, key)
}
func (m mapUnstructuredInterface) Iterate(fn func(key string, value Value) bool) bool {
return m.IterateUsing(HeapAllocator, fn)
}
func (m mapUnstructuredInterface) IterateUsing(a Allocator, fn func(key string, value Value) bool) bool {
if len(m) == 0 {
return true
}
vv := a.allocValueUnstructured()
defer a.Free(vv)
for k, v := range m {
if ks, ok := k.(string); !ok {
continue
} else {
if !fn(ks, vv.reuse(v)) {
return false
}
}
}
return true
}
func (m mapUnstructuredInterface) Length() int {
return len(m)
}
func (m mapUnstructuredInterface) Empty() bool {
return len(m) == 0
}
func (m mapUnstructuredInterface) Equals(other Map) bool {
return m.EqualsUsing(HeapAllocator, other)
}
func (m mapUnstructuredInterface) EqualsUsing(a Allocator, other Map) bool {
lhsLength := m.Length()
rhsLength := other.Length()
if lhsLength != rhsLength {
return false
}
if lhsLength == 0 {
return true
}
vv := a.allocValueUnstructured()
defer a.Free(vv)
return other.IterateUsing(a, func(key string, value Value) bool {
lhsVal, ok := m[key]
if !ok {
return false
}
return EqualsUsing(a, vv.reuse(lhsVal), value)
})
}
func (m mapUnstructuredInterface) Zip(other Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
return m.ZipUsing(HeapAllocator, other, order, fn)
}
func (m mapUnstructuredInterface) ZipUsing(a Allocator, other Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
return defaultMapZip(a, m, other, order, fn)
}
type mapUnstructuredString map[string]interface{}
func (m mapUnstructuredString) Set(key string, val Value) {
m[key] = val.Unstructured()
}
func (m mapUnstructuredString) Get(key string) (Value, bool) {
return m.GetUsing(HeapAllocator, key)
}
func (m mapUnstructuredString) GetUsing(a Allocator, key string) (Value, bool) {
if v, ok := m[key]; !ok {
return nil, false
} else {
return a.allocValueUnstructured().reuse(v), true
}
}
func (m mapUnstructuredString) Has(key string) bool {
_, ok := m[key]
return ok
}
func (m mapUnstructuredString) Delete(key string) {
delete(m, key)
}
func (m mapUnstructuredString) Iterate(fn func(key string, value Value) bool) bool {
return m.IterateUsing(HeapAllocator, fn)
}
func (m mapUnstructuredString) IterateUsing(a Allocator, fn func(key string, value Value) bool) bool {
if len(m) == 0 {
return true
}
vv := a.allocValueUnstructured()
defer a.Free(vv)
for k, v := range m {
if !fn(k, vv.reuse(v)) {
return false
}
}
return true
}
func (m mapUnstructuredString) Length() int {
return len(m)
}
func (m mapUnstructuredString) Equals(other Map) bool {
return m.EqualsUsing(HeapAllocator, other)
}
func (m mapUnstructuredString) EqualsUsing(a Allocator, other Map) bool {
lhsLength := m.Length()
rhsLength := other.Length()
if lhsLength != rhsLength {
return false
}
if lhsLength == 0 {
return true
}
vv := a.allocValueUnstructured()
defer a.Free(vv)
return other.IterateUsing(a, func(key string, value Value) bool {
lhsVal, ok := m[key]
if !ok {
return false
}
return EqualsUsing(a, vv.reuse(lhsVal), value)
})
}
func (m mapUnstructuredString) Zip(other Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
return m.ZipUsing(HeapAllocator, other, order, fn)
}
func (m mapUnstructuredString) ZipUsing(a Allocator, other Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
return defaultMapZip(a, m, other, order, fn)
}
func (m mapUnstructuredString) Empty() bool {
return len(m) == 0
}
+496
View File
@@ -0,0 +1,496 @@
/*
Copyright 2020 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 (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"sort"
"sync"
"sync/atomic"
)
// UnstructuredConverter defines how a type can be converted directly to unstructured.
// Types that implement json.Marshaler may also optionally implement this interface to provide a more
// direct and more efficient conversion. All types that choose to implement this interface must still
// implement this same conversion via json.Marshaler.
type UnstructuredConverter interface {
json.Marshaler // require that json.Marshaler is implemented
// ToUnstructured returns the unstructured representation.
ToUnstructured() interface{}
}
// TypeReflectCacheEntry keeps data gathered using reflection about how a type is converted to/from unstructured.
type TypeReflectCacheEntry struct {
isJsonMarshaler bool
ptrIsJsonMarshaler bool
isJsonUnmarshaler bool
ptrIsJsonUnmarshaler bool
isStringConvertable bool
ptrIsStringConvertable bool
structFields map[string]*FieldCacheEntry
orderedStructFields []*FieldCacheEntry
}
// FieldCacheEntry keeps data gathered using reflection about how the field of a struct is converted to/from
// unstructured.
type FieldCacheEntry struct {
// JsonName returns the name of the field according to the json tags on the struct field.
JsonName string
// isOmitEmpty is true if the field has the json 'omitempty' tag.
isOmitEmpty bool
// omitzero is set if the field has the json 'omitzero' tag.
omitzero func(reflect.Value) bool
// fieldPath is a list of field indices (see FieldByIndex) to lookup the value of
// a field in a reflect.Value struct. The field indices in the list form a path used
// to traverse through intermediary 'inline' fields.
fieldPath [][]int
fieldType reflect.Type
TypeEntry *TypeReflectCacheEntry
}
func (f *FieldCacheEntry) CanOmit(fieldVal reflect.Value) bool {
if f.isOmitEmpty && (safeIsNil(fieldVal) || isEmpty(fieldVal)) {
return true
}
if f.omitzero != nil && f.omitzero(fieldVal) {
return true
}
return false
}
// GetFrom returns the field identified by this FieldCacheEntry from the provided struct.
func (f *FieldCacheEntry) GetFrom(structVal reflect.Value) reflect.Value {
// field might be nested within 'inline' structs
for _, elem := range f.fieldPath {
if safeIsNil(structVal) {
// if any part of the path is nil, return the zero value for the field type
return reflect.Zero(f.fieldType)
}
structVal = dereference(structVal).FieldByIndex(elem)
}
return structVal
}
var marshalerType = reflect.TypeOf(new(json.Marshaler)).Elem()
var unmarshalerType = reflect.TypeOf(new(json.Unmarshaler)).Elem()
var unstructuredConvertableType = reflect.TypeOf(new(UnstructuredConverter)).Elem()
var defaultReflectCache = newReflectCache()
// TypeReflectEntryOf returns the TypeReflectCacheEntry of the provided reflect.Type.
func TypeReflectEntryOf(t reflect.Type) *TypeReflectCacheEntry {
cm := defaultReflectCache.get()
if record, ok := cm[t]; ok {
return record
}
updates := reflectCacheMap{}
result := typeReflectEntryOf(cm, t, updates)
if len(updates) > 0 {
defaultReflectCache.update(updates)
}
return result
}
// TypeReflectEntryOf returns all updates needed to add provided reflect.Type, and the types its fields transitively
// depend on, to the cache.
func typeReflectEntryOf(cm reflectCacheMap, t reflect.Type, updates reflectCacheMap) *TypeReflectCacheEntry {
if record, ok := cm[t]; ok {
return record
}
if record, ok := updates[t]; ok {
return record
}
typeEntry := &TypeReflectCacheEntry{
isJsonMarshaler: t.Implements(marshalerType),
ptrIsJsonMarshaler: reflect.PtrTo(t).Implements(marshalerType),
isJsonUnmarshaler: reflect.PtrTo(t).Implements(unmarshalerType),
isStringConvertable: t.Implements(unstructuredConvertableType),
ptrIsStringConvertable: reflect.PtrTo(t).Implements(unstructuredConvertableType),
}
if t.Kind() == reflect.Struct {
fieldEntries := map[string]*FieldCacheEntry{}
buildStructCacheEntry(t, fieldEntries, nil)
typeEntry.structFields = fieldEntries
sortedByJsonName := make([]*FieldCacheEntry, len(fieldEntries))
i := 0
for _, entry := range fieldEntries {
sortedByJsonName[i] = entry
i++
}
sort.Slice(sortedByJsonName, func(i, j int) bool {
return sortedByJsonName[i].JsonName < sortedByJsonName[j].JsonName
})
typeEntry.orderedStructFields = sortedByJsonName
}
// cyclic type references are allowed, so we must add the typeEntry to the updates map before resolving
// the field.typeEntry references, or creating them if they are not already in the cache
updates[t] = typeEntry
for _, field := range typeEntry.structFields {
if field.TypeEntry == nil {
field.TypeEntry = typeReflectEntryOf(cm, field.fieldType, updates)
}
}
return typeEntry
}
func buildStructCacheEntry(t reflect.Type, infos map[string]*FieldCacheEntry, fieldPath [][]int) {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonName, omit, isInline, isOmitempty, omitzero := lookupJsonTags(field)
if omit {
continue
}
if isInline {
e := field.Type
if field.Type.Kind() == reflect.Ptr {
e = field.Type.Elem()
}
if e.Kind() == reflect.Struct {
buildStructCacheEntry(e, infos, append(fieldPath, field.Index))
}
continue
}
info := &FieldCacheEntry{JsonName: jsonName, isOmitEmpty: isOmitempty, omitzero: omitzero, fieldPath: append(fieldPath, field.Index), fieldType: field.Type}
infos[jsonName] = info
}
}
// Fields returns a map of JSON field name to FieldCacheEntry for structs, or nil for non-structs.
func (e TypeReflectCacheEntry) Fields() map[string]*FieldCacheEntry {
return e.structFields
}
// Fields returns a map of JSON field name to FieldCacheEntry for structs, or nil for non-structs.
func (e TypeReflectCacheEntry) OrderedFields() []*FieldCacheEntry {
return e.orderedStructFields
}
// CanConvertToUnstructured returns true if this TypeReflectCacheEntry can convert values of its type to unstructured.
func (e TypeReflectCacheEntry) CanConvertToUnstructured() bool {
return e.isJsonMarshaler || e.ptrIsJsonMarshaler || e.isStringConvertable || e.ptrIsStringConvertable
}
// ToUnstructured converts the provided value to unstructured and returns it.
func (e TypeReflectCacheEntry) ToUnstructured(sv reflect.Value) (interface{}, error) {
// This is based on https://github.com/kubernetes/kubernetes/blob/82c9e5c814eb7acc6cc0a090c057294d0667ad66/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go#L505
// and is intended to replace it.
// Check if the object is a nil pointer.
if sv.Kind() == reflect.Ptr && sv.IsNil() {
// We're done - we don't need to store anything.
return nil, nil
}
// Check if the object has a custom string converter and use it if available, since it is much more efficient
// than round tripping through json.
if converter, ok := e.getUnstructuredConverter(sv); ok {
return converter.ToUnstructured(), nil
}
// Check if the object has a custom JSON marshaller/unmarshaller.
if marshaler, ok := e.getJsonMarshaler(sv); ok {
data, err := marshaler.MarshalJSON()
if err != nil {
return nil, err
}
switch {
case len(data) == 0:
return nil, fmt.Errorf("error decoding from json: empty value")
case bytes.Equal(data, nullBytes):
// We're done - we don't need to store anything.
return nil, nil
case bytes.Equal(data, trueBytes):
return true, nil
case bytes.Equal(data, falseBytes):
return false, nil
case data[0] == '"':
var result string
err := unmarshal(data, &result)
if err != nil {
return nil, fmt.Errorf("error decoding string from json: %v", err)
}
return result, nil
case data[0] == '{':
result := make(map[string]interface{})
err := unmarshal(data, &result)
if err != nil {
return nil, fmt.Errorf("error decoding object from json: %v", err)
}
return result, nil
case data[0] == '[':
result := make([]interface{}, 0)
err := unmarshal(data, &result)
if err != nil {
return nil, fmt.Errorf("error decoding array from json: %v", err)
}
return result, nil
default:
var (
resultInt int64
resultFloat float64
err error
)
if err = unmarshal(data, &resultInt); err == nil {
return resultInt, nil
} else if err = unmarshal(data, &resultFloat); err == nil {
return resultFloat, nil
} else {
return nil, fmt.Errorf("error decoding number from json: %v", err)
}
}
}
return nil, fmt.Errorf("provided type cannot be converted: %v", sv.Type())
}
// CanConvertFromUnstructured returns true if this TypeReflectCacheEntry can convert objects of the type from unstructured.
func (e TypeReflectCacheEntry) CanConvertFromUnstructured() bool {
return e.isJsonUnmarshaler
}
// FromUnstructured converts the provided source value from unstructured into the provided destination value.
func (e TypeReflectCacheEntry) FromUnstructured(sv, dv reflect.Value) error {
// TODO: this could be made much more efficient using direct conversions like
// UnstructuredConverter.ToUnstructured provides.
st := dv.Type()
data, err := json.Marshal(sv.Interface())
if err != nil {
return fmt.Errorf("error encoding %s to json: %v", st.String(), err)
}
if unmarshaler, ok := e.getJsonUnmarshaler(dv); ok {
return unmarshaler.UnmarshalJSON(data)
}
return fmt.Errorf("unable to unmarshal %v into %v", sv.Type(), dv.Type())
}
var (
nullBytes = []byte("null")
trueBytes = []byte("true")
falseBytes = []byte("false")
)
func (e TypeReflectCacheEntry) getJsonMarshaler(v reflect.Value) (json.Marshaler, bool) {
if e.isJsonMarshaler {
return v.Interface().(json.Marshaler), true
}
if e.ptrIsJsonMarshaler {
// Check pointer receivers if v is not a pointer
if v.Kind() != reflect.Ptr && v.CanAddr() {
v = v.Addr()
return v.Interface().(json.Marshaler), true
}
}
return nil, false
}
func (e TypeReflectCacheEntry) getJsonUnmarshaler(v reflect.Value) (json.Unmarshaler, bool) {
if !e.isJsonUnmarshaler {
return nil, false
}
return v.Addr().Interface().(json.Unmarshaler), true
}
func (e TypeReflectCacheEntry) getUnstructuredConverter(v reflect.Value) (UnstructuredConverter, bool) {
if e.isStringConvertable {
return v.Interface().(UnstructuredConverter), true
}
if e.ptrIsStringConvertable {
// Check pointer receivers if v is not a pointer
if v.CanAddr() {
v = v.Addr()
return v.Interface().(UnstructuredConverter), true
}
}
return nil, false
}
type typeReflectCache struct {
// use an atomic and copy-on-write since there are a fixed (typically very small) number of structs compiled into any
// go program using this cache
value atomic.Value
// mu is held by writers when performing load/modify/store operations on the cache, readers do not need to hold a
// read-lock since the atomic value is always read-only
mu sync.Mutex
}
func newReflectCache() *typeReflectCache {
cache := &typeReflectCache{}
cache.value.Store(make(reflectCacheMap))
return cache
}
type reflectCacheMap map[reflect.Type]*TypeReflectCacheEntry
// get returns the reflectCacheMap.
func (c *typeReflectCache) get() reflectCacheMap {
return c.value.Load().(reflectCacheMap)
}
// update merges the provided updates into the cache.
func (c *typeReflectCache) update(updates reflectCacheMap) {
c.mu.Lock()
defer c.mu.Unlock()
currentCacheMap := c.value.Load().(reflectCacheMap)
hasNewEntries := false
for t := range updates {
if _, ok := currentCacheMap[t]; !ok {
hasNewEntries = true
break
}
}
if !hasNewEntries {
// Bail if the updates have been set while waiting for lock acquisition.
// This is safe since setting entries is idempotent.
return
}
newCacheMap := make(reflectCacheMap, len(currentCacheMap)+len(updates))
for k, v := range currentCacheMap {
newCacheMap[k] = v
}
for t, update := range updates {
newCacheMap[t] = update
}
c.value.Store(newCacheMap)
}
// Below json Unmarshal is fromk8s.io/apimachinery/pkg/util/json
// to handle number conversions as expected by Kubernetes
// limit recursive depth to prevent stack overflow errors
const maxDepth = 10000
// unmarshal unmarshals the given data
// If v is a *map[string]interface{}, numbers are converted to int64 or float64
func unmarshal(data []byte, v interface{}) error {
// Build a decoder from the given data
decoder := json.NewDecoder(bytes.NewBuffer(data))
// Preserve numbers, rather than casting to float64 automatically
decoder.UseNumber()
// Run the decode
if err := decoder.Decode(v); err != nil {
return err
}
next := decoder.InputOffset()
if _, err := decoder.Token(); !errors.Is(err, io.EOF) {
tail := bytes.TrimLeft(data[next:], " \t\r\n")
return fmt.Errorf("unexpected trailing data at offset %d", len(data)-len(tail))
}
// If the decode succeeds, post-process the object to convert json.Number objects to int64 or float64
switch v := v.(type) {
case *map[string]interface{}:
return convertMapNumbers(*v, 0)
case *[]interface{}:
return convertSliceNumbers(*v, 0)
case *interface{}:
return convertInterfaceNumbers(v, 0)
default:
return nil
}
}
func convertInterfaceNumbers(v *interface{}, depth int) error {
var err error
switch v2 := (*v).(type) {
case json.Number:
*v, err = convertNumber(v2)
case map[string]interface{}:
err = convertMapNumbers(v2, depth+1)
case []interface{}:
err = convertSliceNumbers(v2, depth+1)
}
return err
}
// convertMapNumbers traverses the map, converting any json.Number values to int64 or float64.
// values which are map[string]interface{} or []interface{} are recursively visited
func convertMapNumbers(m map[string]interface{}, depth int) error {
if depth > maxDepth {
return fmt.Errorf("exceeded max depth of %d", maxDepth)
}
var err error
for k, v := range m {
switch v := v.(type) {
case json.Number:
m[k], err = convertNumber(v)
case map[string]interface{}:
err = convertMapNumbers(v, depth+1)
case []interface{}:
err = convertSliceNumbers(v, depth+1)
}
if err != nil {
return err
}
}
return nil
}
// convertSliceNumbers traverses the slice, converting any json.Number values to int64 or float64.
// values which are map[string]interface{} or []interface{} are recursively visited
func convertSliceNumbers(s []interface{}, depth int) error {
if depth > maxDepth {
return fmt.Errorf("exceeded max depth of %d", maxDepth)
}
var err error
for i, v := range s {
switch v := v.(type) {
case json.Number:
s[i], err = convertNumber(v)
case map[string]interface{}:
err = convertMapNumbers(v, depth+1)
case []interface{}:
err = convertSliceNumbers(v, depth+1)
}
if err != nil {
return err
}
}
return nil
}
// convertNumber converts a json.Number to an int64 or float64, or returns an error
func convertNumber(n json.Number) (interface{}, error) {
// Attempt to convert to an int64 first
if i, err := n.Int64(); err == nil {
return i, nil
}
// Return a float64 (default json.Decode() behavior)
// An overflow will return an error
return n.Float64()
}
+50
View File
@@ -0,0 +1,50 @@
/*
Copyright 2019 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
// Compare compares floats. The result will be 0 if lhs==rhs, -1 if f <
// rhs, and +1 if f > rhs.
func FloatCompare(lhs, rhs float64) int {
if lhs > rhs {
return 1
} else if lhs < rhs {
return -1
}
return 0
}
// IntCompare compares integers. The result will be 0 if i==rhs, -1 if i <
// rhs, and +1 if i > rhs.
func IntCompare(lhs, rhs int64) int {
if lhs > rhs {
return 1
} else if lhs < rhs {
return -1
}
return 0
}
// Compare compares booleans. The result will be 0 if b==rhs, -1 if b <
// rhs, and +1 if b > rhs.
func BoolCompare(lhs, rhs bool) int {
if lhs == rhs {
return 0
} else if !lhs {
return -1
}
return 1
}
+208
View File
@@ -0,0 +1,208 @@
/*
Copyright 2019 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"
"reflect"
)
type structReflect struct {
valueReflect
}
func (r structReflect) Length() int {
i := 0
eachStructField(r.Value, func(_ *TypeReflectCacheEntry, s string, value reflect.Value) bool {
i++
return true
})
return i
}
func (r structReflect) Empty() bool {
return eachStructField(r.Value, func(_ *TypeReflectCacheEntry, s string, value reflect.Value) bool {
return false // exit early if the struct is non-empty
})
}
func (r structReflect) Get(key string) (Value, bool) {
return r.GetUsing(HeapAllocator, key)
}
func (r structReflect) GetUsing(a Allocator, key string) (Value, bool) {
if val, ok := r.findJsonNameField(key); ok {
return a.allocValueReflect().mustReuse(val, nil, nil, nil), true
}
return nil, false
}
func (r structReflect) Has(key string) bool {
_, ok := r.findJsonNameField(key)
return ok
}
func (r structReflect) Set(key string, val Value) {
fieldEntry, ok := TypeReflectEntryOf(r.Value.Type()).Fields()[key]
if !ok {
panic(fmt.Sprintf("key %s may not be set on struct %T: field does not exist", key, r.Value.Interface()))
}
oldVal := fieldEntry.GetFrom(r.Value)
newVal := reflect.ValueOf(val.Unstructured())
r.update(fieldEntry, key, oldVal, newVal)
}
func (r structReflect) Delete(key string) {
fieldEntry, ok := TypeReflectEntryOf(r.Value.Type()).Fields()[key]
if !ok {
panic(fmt.Sprintf("key %s may not be deleted on struct %T: field does not exist", key, r.Value.Interface()))
}
oldVal := fieldEntry.GetFrom(r.Value)
if oldVal.Kind() != reflect.Ptr && !fieldEntry.isOmitEmpty {
panic(fmt.Sprintf("key %s may not be deleted on struct: %T: value is neither a pointer nor an omitempty field", key, r.Value.Interface()))
}
r.update(fieldEntry, key, oldVal, reflect.Zero(oldVal.Type()))
}
func (r structReflect) update(fieldEntry *FieldCacheEntry, key string, oldVal, newVal reflect.Value) {
if oldVal.CanSet() {
oldVal.Set(newVal)
return
}
// map items are not addressable, so if a struct is contained in a map, the only way to modify it is
// to write a replacement fieldEntry into the map.
if r.ParentMap != nil {
if r.ParentMapKey == nil {
panic("ParentMapKey must not be nil if ParentMap is not nil")
}
replacement := reflect.New(r.Value.Type()).Elem()
fieldEntry.GetFrom(replacement).Set(newVal)
r.ParentMap.SetMapIndex(*r.ParentMapKey, replacement)
return
}
// This should never happen since NewValueReflect ensures that the root object reflected on is a pointer and map
// item replacement is handled above.
panic(fmt.Sprintf("key %s may not be modified on struct: %T: struct is not settable", key, r.Value.Interface()))
}
func (r structReflect) Iterate(fn func(string, Value) bool) bool {
return r.IterateUsing(HeapAllocator, fn)
}
func (r structReflect) IterateUsing(a Allocator, fn func(string, Value) bool) bool {
vr := a.allocValueReflect()
defer a.Free(vr)
return eachStructField(r.Value, func(e *TypeReflectCacheEntry, s string, value reflect.Value) bool {
return fn(s, vr.mustReuse(value, e, nil, nil))
})
}
func eachStructField(structVal reflect.Value, fn func(*TypeReflectCacheEntry, string, reflect.Value) bool) bool {
for _, fieldCacheEntry := range TypeReflectEntryOf(structVal.Type()).OrderedFields() {
fieldVal := fieldCacheEntry.GetFrom(structVal)
if fieldCacheEntry.CanOmit(fieldVal) {
// omit it
continue
}
ok := fn(fieldCacheEntry.TypeEntry, fieldCacheEntry.JsonName, fieldVal)
if !ok {
return false
}
}
return true
}
func (r structReflect) Unstructured() interface{} {
// Use number of struct fields as a cheap way to rough estimate map size
result := make(map[string]interface{}, r.Value.NumField())
r.Iterate(func(s string, value Value) bool {
result[s] = value.Unstructured()
return true
})
return result
}
func (r structReflect) Equals(m Map) bool {
return r.EqualsUsing(HeapAllocator, m)
}
func (r structReflect) EqualsUsing(a Allocator, m Map) bool {
// MapEquals uses zip and is fairly efficient for structReflect
return MapEqualsUsing(a, &r, m)
}
func (r structReflect) findJsonNameFieldAndNotEmpty(jsonName string) (reflect.Value, bool) {
structCacheEntry, ok := TypeReflectEntryOf(r.Value.Type()).Fields()[jsonName]
if !ok {
return reflect.Value{}, false
}
fieldVal := structCacheEntry.GetFrom(r.Value)
return fieldVal, !structCacheEntry.CanOmit(fieldVal)
}
func (r structReflect) findJsonNameField(jsonName string) (val reflect.Value, ok bool) {
structCacheEntry, ok := TypeReflectEntryOf(r.Value.Type()).Fields()[jsonName]
if !ok {
return reflect.Value{}, false
}
fieldVal := structCacheEntry.GetFrom(r.Value)
return fieldVal, !structCacheEntry.CanOmit(fieldVal)
}
func (r structReflect) Zip(other Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
return r.ZipUsing(HeapAllocator, other, order, fn)
}
func (r structReflect) ZipUsing(a Allocator, other Map, order MapTraverseOrder, fn func(key string, lhs, rhs Value) bool) bool {
if otherStruct, ok := other.(*structReflect); ok && r.Value.Type() == otherStruct.Value.Type() {
lhsvr, rhsvr := a.allocValueReflect(), a.allocValueReflect()
defer a.Free(lhsvr)
defer a.Free(rhsvr)
return r.structZip(otherStruct, lhsvr, rhsvr, fn)
}
return defaultMapZip(a, &r, other, order, fn)
}
// structZip provides an optimized zip for structReflect types. The zip is always lexical key ordered since there is
// no additional cost to ordering the zip for structured types.
func (r structReflect) structZip(other *structReflect, lhsvr, rhsvr *valueReflect, fn func(key string, lhs, rhs Value) bool) bool {
lhsVal := r.Value
rhsVal := other.Value
for _, fieldCacheEntry := range TypeReflectEntryOf(lhsVal.Type()).OrderedFields() {
lhsFieldVal := fieldCacheEntry.GetFrom(lhsVal)
rhsFieldVal := fieldCacheEntry.GetFrom(rhsVal)
lhsOmit := fieldCacheEntry.CanOmit(lhsFieldVal)
rhsOmit := fieldCacheEntry.CanOmit(rhsFieldVal)
if lhsOmit && rhsOmit {
continue
}
var lhsVal, rhsVal Value
if !lhsOmit {
lhsVal = lhsvr.mustReuse(lhsFieldVal, fieldCacheEntry.TypeEntry, nil, nil)
}
if !rhsOmit {
rhsVal = rhsvr.mustReuse(rhsFieldVal, fieldCacheEntry.TypeEntry, nil, nil)
}
if !fn(fieldCacheEntry.JsonName, lhsVal, rhsVal) {
return false
}
}
return true
}
+352
View File
@@ -0,0 +1,352 @@
/*
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 (
"bytes"
"fmt"
"io"
"strings"
jsoniter "github.com/json-iterator/go"
yaml "go.yaml.in/yaml/v2"
)
var (
readPool = jsoniter.NewIterator(jsoniter.ConfigCompatibleWithStandardLibrary).Pool()
writePool = jsoniter.NewStream(jsoniter.ConfigCompatibleWithStandardLibrary, nil, 1024).Pool()
)
// A Value corresponds to an 'atom' in the schema. It should return true
// for at least one of the IsXXX methods below, or the value is
// considered "invalid"
type Value interface {
// IsMap returns true if the Value is a Map, false otherwise.
IsMap() bool
// IsList returns true if the Value is a List, false otherwise.
IsList() bool
// IsBool returns true if the Value is a bool, false otherwise.
IsBool() bool
// IsInt returns true if the Value is a int64, false otherwise.
IsInt() bool
// IsFloat returns true if the Value is a float64, false
// otherwise.
IsFloat() bool
// IsString returns true if the Value is a string, false
// otherwise.
IsString() bool
// IsMap returns true if the Value is null, false otherwise.
IsNull() bool
// AsMap converts the Value into a Map (or panic if the type
// doesn't allow it).
AsMap() Map
// AsMapUsing uses the provided allocator and converts the Value
// into a Map (or panic if the type doesn't allow it).
AsMapUsing(Allocator) Map
// AsList converts the Value into a List (or panic if the type
// doesn't allow it).
AsList() List
// AsListUsing uses the provided allocator and converts the Value
// into a List (or panic if the type doesn't allow it).
AsListUsing(Allocator) List
// AsBool converts the Value into a bool (or panic if the type
// doesn't allow it).
AsBool() bool
// AsInt converts the Value into an int64 (or panic if the type
// doesn't allow it).
AsInt() int64
// AsFloat converts the Value into a float64 (or panic if the type
// doesn't allow it).
AsFloat() float64
// AsString converts the Value into a string (or panic if the type
// doesn't allow it).
AsString() string
// Unstructured converts the Value into an Unstructured interface{}.
Unstructured() interface{}
}
// FromJSON is a helper function for reading a JSON document.
func FromJSON(input []byte) (Value, error) {
return FromJSONFast(input)
}
// FromJSONFast is a helper function for reading a JSON document.
func FromJSONFast(input []byte) (Value, error) {
iter := readPool.BorrowIterator(input)
defer readPool.ReturnIterator(iter)
return readJSONIter(iter)
}
// ToJSON is a helper function for producing a JSon document.
func ToJSON(v Value) ([]byte, error) {
buf := bytes.Buffer{}
stream := writePool.BorrowStream(&buf)
defer writePool.ReturnStream(stream)
writeJSONStream(v, stream)
b := stream.Buffer()
err := stream.Flush()
// Help jsoniter manage its buffers--without this, the next
// use of the stream is likely to require an allocation. Look
// at the jsoniter stream code to understand why. They were probably
// optimizing for folks using the buffer directly.
stream.SetBuffer(b[:0])
return buf.Bytes(), err
}
// readJSONIter reads a Value from a JSON iterator.
// DO NOT EXPORT
// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202
func readJSONIter(iter *jsoniter.Iterator) (Value, error) {
v := iter.Read()
if iter.Error != nil && iter.Error != io.EOF {
return nil, iter.Error
}
return NewValueInterface(v), nil
}
// writeJSONStream writes a value into a JSON stream.
// DO NOT EXPORT
// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202
func writeJSONStream(v Value, stream *jsoniter.Stream) {
stream.WriteVal(v.Unstructured())
}
// ToYAML marshals a value as YAML.
func ToYAML(v Value) ([]byte, error) {
return yaml.Marshal(v.Unstructured())
}
// Equals returns true iff the two values are equal.
func Equals(lhs, rhs Value) bool {
return EqualsUsing(HeapAllocator, lhs, rhs)
}
// EqualsUsing uses the provided allocator and returns true iff the two values are equal.
func EqualsUsing(a Allocator, lhs, rhs Value) bool {
if lhs.IsFloat() || rhs.IsFloat() {
var lf float64
if lhs.IsFloat() {
lf = lhs.AsFloat()
} else if lhs.IsInt() {
lf = float64(lhs.AsInt())
} else {
return false
}
var rf float64
if rhs.IsFloat() {
rf = rhs.AsFloat()
} else if rhs.IsInt() {
rf = float64(rhs.AsInt())
} else {
return false
}
return lf == rf
}
if lhs.IsInt() {
if rhs.IsInt() {
return lhs.AsInt() == rhs.AsInt()
}
return false
} else if rhs.IsInt() {
return false
}
if lhs.IsString() {
if rhs.IsString() {
return lhs.AsString() == rhs.AsString()
}
return false
} else if rhs.IsString() {
return false
}
if lhs.IsBool() {
if rhs.IsBool() {
return lhs.AsBool() == rhs.AsBool()
}
return false
} else if rhs.IsBool() {
return false
}
if lhs.IsList() {
if rhs.IsList() {
lhsList := lhs.AsListUsing(a)
defer a.Free(lhsList)
rhsList := rhs.AsListUsing(a)
defer a.Free(rhsList)
return lhsList.EqualsUsing(a, rhsList)
}
return false
} else if rhs.IsList() {
return false
}
if lhs.IsMap() {
if rhs.IsMap() {
lhsList := lhs.AsMapUsing(a)
defer a.Free(lhsList)
rhsList := rhs.AsMapUsing(a)
defer a.Free(rhsList)
return lhsList.EqualsUsing(a, rhsList)
}
return false
} else if rhs.IsMap() {
return false
}
if lhs.IsNull() {
if rhs.IsNull() {
return true
}
return false
} else if rhs.IsNull() {
return false
}
// No field is set, on either objects.
return true
}
// ToString returns a human-readable representation of the value.
func ToString(v Value) string {
if v.IsNull() {
return "null"
}
switch {
case v.IsFloat():
return fmt.Sprintf("%v", v.AsFloat())
case v.IsInt():
return fmt.Sprintf("%v", v.AsInt())
case v.IsString():
return fmt.Sprintf("%q", v.AsString())
case v.IsBool():
return fmt.Sprintf("%v", v.AsBool())
case v.IsList():
strs := []string{}
list := v.AsList()
for i := 0; i < list.Length(); i++ {
strs = append(strs, ToString(list.At(i)))
}
return "[" + strings.Join(strs, ",") + "]"
case v.IsMap():
strs := []string{}
v.AsMap().Iterate(func(k string, v Value) bool {
strs = append(strs, fmt.Sprintf("%v=%v", k, ToString(v)))
return true
})
return strings.Join(strs, "")
}
// No field is set, on either objects.
return "{{undefined}}"
}
// Less provides a total ordering for Value (so that they can be sorted, even
// if they are of different types).
func Less(lhs, rhs Value) bool {
return Compare(lhs, rhs) == -1
}
// Compare provides a total ordering for Value (so that they can be
// sorted, even if they are of different types). The result will be 0 if
// v==rhs, -1 if v < rhs, and +1 if v > rhs.
func Compare(lhs, rhs Value) int {
return CompareUsing(HeapAllocator, lhs, rhs)
}
// CompareUsing uses the provided allocator and provides a total
// ordering for Value (so that they can be sorted, even if they
// are of different types). The result will be 0 if v==rhs, -1
// if v < rhs, and +1 if v > rhs.
func CompareUsing(a Allocator, lhs, rhs Value) int {
if lhs.IsFloat() {
if !rhs.IsFloat() {
// Extra: compare floats and ints numerically.
if rhs.IsInt() {
return FloatCompare(lhs.AsFloat(), float64(rhs.AsInt()))
}
return -1
}
return FloatCompare(lhs.AsFloat(), rhs.AsFloat())
} else if rhs.IsFloat() {
// Extra: compare floats and ints numerically.
if lhs.IsInt() {
return FloatCompare(float64(lhs.AsInt()), rhs.AsFloat())
}
return 1
}
if lhs.IsInt() {
if !rhs.IsInt() {
return -1
}
return IntCompare(lhs.AsInt(), rhs.AsInt())
} else if rhs.IsInt() {
return 1
}
if lhs.IsString() {
if !rhs.IsString() {
return -1
}
return strings.Compare(lhs.AsString(), rhs.AsString())
} else if rhs.IsString() {
return 1
}
if lhs.IsBool() {
if !rhs.IsBool() {
return -1
}
return BoolCompare(lhs.AsBool(), rhs.AsBool())
} else if rhs.IsBool() {
return 1
}
if lhs.IsList() {
if !rhs.IsList() {
return -1
}
lhsList := lhs.AsListUsing(a)
defer a.Free(lhsList)
rhsList := rhs.AsListUsing(a)
defer a.Free(rhsList)
return ListCompareUsing(a, lhsList, rhsList)
} else if rhs.IsList() {
return 1
}
if lhs.IsMap() {
if !rhs.IsMap() {
return -1
}
lhsMap := lhs.AsMapUsing(a)
defer a.Free(lhsMap)
rhsMap := rhs.AsMapUsing(a)
defer a.Free(rhsMap)
return MapCompareUsing(a, lhsMap, rhsMap)
} else if rhs.IsMap() {
return 1
}
if lhs.IsNull() {
if !rhs.IsNull() {
return -1
}
return 0
} else if rhs.IsNull() {
return 1
}
// Invalid Value-- nothing is set.
return 0
}
+294
View File
@@ -0,0 +1,294 @@
/*
Copyright 2019 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/base64"
"fmt"
"reflect"
)
// NewValueReflect creates a Value backed by an "interface{}" type,
// typically an structured object in Kubernetes world that is uses reflection to expose.
// The provided "interface{}" value must be a pointer so that the value can be modified via reflection.
// The provided "interface{}" may contain structs and types that are converted to Values
// by the jsonMarshaler interface.
func NewValueReflect(value interface{}) (Value, error) {
if value == nil {
return NewValueInterface(nil), nil
}
v := reflect.ValueOf(value)
if v.Kind() != reflect.Ptr {
// The root value to reflect on must be a pointer so that map.Set() and map.Delete() operations are possible.
return nil, fmt.Errorf("value provided to NewValueReflect must be a pointer")
}
return wrapValueReflect(v, nil, nil)
}
// wrapValueReflect wraps the provide reflect.Value as a value. If parent in the data tree is a map, parentMap
// and parentMapKey must be provided so that the returned value may be set and deleted.
func wrapValueReflect(value reflect.Value, parentMap, parentMapKey *reflect.Value) (Value, error) {
val := HeapAllocator.allocValueReflect()
return val.reuse(value, nil, parentMap, parentMapKey)
}
// wrapValueReflect wraps the provide reflect.Value as a value, and panics if there is an error. If parent in the data
// tree is a map, parentMap and parentMapKey must be provided so that the returned value may be set and deleted.
func mustWrapValueReflect(value reflect.Value, parentMap, parentMapKey *reflect.Value) Value {
v, err := wrapValueReflect(value, parentMap, parentMapKey)
if err != nil {
panic(err)
}
return v
}
// the value interface doesn't care about the type for value.IsNull, so we can use a constant
var nilType = reflect.TypeOf(&struct{}{})
// reuse replaces the value of the valueReflect. If parent in the data tree is a map, parentMap and parentMapKey
// must be provided so that the returned value may be set and deleted.
func (r *valueReflect) reuse(value reflect.Value, cacheEntry *TypeReflectCacheEntry, parentMap, parentMapKey *reflect.Value) (Value, error) {
if cacheEntry == nil {
cacheEntry = TypeReflectEntryOf(value.Type())
}
if cacheEntry.CanConvertToUnstructured() {
u, err := cacheEntry.ToUnstructured(value)
if err != nil {
return nil, err
}
if u == nil {
value = reflect.Zero(nilType)
} else {
value = reflect.ValueOf(u)
}
}
r.Value = dereference(value)
r.ParentMap = parentMap
r.ParentMapKey = parentMapKey
r.kind = kind(r.Value)
return r, nil
}
// mustReuse replaces the value of the valueReflect and panics if there is an error. If parent in the data tree is a
// map, parentMap and parentMapKey must be provided so that the returned value may be set and deleted.
func (r *valueReflect) mustReuse(value reflect.Value, cacheEntry *TypeReflectCacheEntry, parentMap, parentMapKey *reflect.Value) Value {
v, err := r.reuse(value, cacheEntry, parentMap, parentMapKey)
if err != nil {
panic(err)
}
return v
}
func dereference(val reflect.Value) reflect.Value {
kind := val.Kind()
if (kind == reflect.Interface || kind == reflect.Ptr) && !safeIsNil(val) {
return val.Elem()
}
return val
}
type valueReflect struct {
ParentMap *reflect.Value
ParentMapKey *reflect.Value
Value reflect.Value
kind reflectType
}
func (r valueReflect) IsMap() bool {
return r.kind == mapType || r.kind == structMapType
}
func (r valueReflect) IsList() bool {
return r.kind == listType
}
func (r valueReflect) IsBool() bool {
return r.kind == boolType
}
func (r valueReflect) IsInt() bool {
return r.kind == intType || r.kind == uintType
}
func (r valueReflect) IsFloat() bool {
return r.kind == floatType
}
func (r valueReflect) IsString() bool {
return r.kind == stringType || r.kind == byteStringType
}
func (r valueReflect) IsNull() bool {
return r.kind == nullType
}
type reflectType = int
const (
mapType = iota
structMapType
listType
intType
uintType
floatType
stringType
byteStringType
boolType
nullType
)
func kind(v reflect.Value) reflectType {
typ := v.Type()
rk := typ.Kind()
switch rk {
case reflect.Map:
if v.IsNil() {
return nullType
}
return mapType
case reflect.Struct:
return structMapType
case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
return intType
case reflect.Uint, reflect.Uint32, reflect.Uint16, reflect.Uint8:
// Uint64 deliberately excluded, see valueUnstructured.Int.
return uintType
case reflect.Float64, reflect.Float32:
return floatType
case reflect.String:
return stringType
case reflect.Bool:
return boolType
case reflect.Slice:
if v.IsNil() {
return nullType
}
elemKind := typ.Elem().Kind()
if elemKind == reflect.Uint8 {
return byteStringType
}
return listType
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.UnsafePointer, reflect.Interface:
if v.IsNil() {
return nullType
}
panic(fmt.Sprintf("unsupported type: %v", v.Type()))
default:
panic(fmt.Sprintf("unsupported type: %v", v.Type()))
}
}
// TODO find a cleaner way to avoid panics from reflect.IsNil()
func safeIsNil(v reflect.Value) bool {
k := v.Kind()
switch k {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
return v.IsNil()
}
return false
}
func (r valueReflect) AsMap() Map {
return r.AsMapUsing(HeapAllocator)
}
func (r valueReflect) AsMapUsing(a Allocator) Map {
switch r.kind {
case structMapType:
v := a.allocStructReflect()
v.valueReflect = r
return v
case mapType:
v := a.allocMapReflect()
v.valueReflect = r
return v
default:
panic("value is not a map or struct")
}
}
func (r valueReflect) AsList() List {
return r.AsListUsing(HeapAllocator)
}
func (r valueReflect) AsListUsing(a Allocator) List {
if r.IsList() {
v := a.allocListReflect()
v.Value = r.Value
return v
}
panic("value is not a list")
}
func (r valueReflect) AsBool() bool {
if r.IsBool() {
return r.Value.Bool()
}
panic("value is not a bool")
}
func (r valueReflect) AsInt() int64 {
if r.kind == intType {
return r.Value.Int()
}
if r.kind == uintType {
return int64(r.Value.Uint())
}
panic("value is not an int")
}
func (r valueReflect) AsFloat() float64 {
if r.IsFloat() {
return r.Value.Float()
}
panic("value is not a float")
}
func (r valueReflect) AsString() string {
switch r.kind {
case stringType:
return r.Value.String()
case byteStringType:
return base64.StdEncoding.EncodeToString(r.Value.Bytes())
}
panic("value is not a string")
}
func (r valueReflect) Unstructured() interface{} {
val := r.Value
switch {
case r.IsNull():
return nil
case val.Kind() == reflect.Struct:
return structReflect{r}.Unstructured()
case val.Kind() == reflect.Map:
return mapReflect{valueReflect: r}.Unstructured()
case r.IsList():
return listReflect{r.Value}.Unstructured()
case r.IsString():
return r.AsString()
case r.IsInt():
return r.AsInt()
case r.IsBool():
return r.AsBool()
case r.IsFloat():
return r.AsFloat()
default:
panic(fmt.Sprintf("value of type %s is not a supported by value reflector", val.Type()))
}
}
+178
View File
@@ -0,0 +1,178 @@
/*
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"
)
// NewValueInterface creates a Value backed by an "interface{}" type,
// typically an unstructured object in Kubernetes world.
// interface{} must be one of: map[string]interface{}, map[interface{}]interface{}, []interface{}, int types, float types,
// string or boolean. Nested interface{} must also be one of these types.
func NewValueInterface(v interface{}) Value {
return Value(HeapAllocator.allocValueUnstructured().reuse(v))
}
type valueUnstructured struct {
Value interface{}
}
// reuse replaces the value of the valueUnstructured.
func (vi *valueUnstructured) reuse(value interface{}) Value {
vi.Value = value
return vi
}
func (v valueUnstructured) IsMap() bool {
if _, ok := v.Value.(map[string]interface{}); ok {
return true
}
if _, ok := v.Value.(map[interface{}]interface{}); ok {
return true
}
return false
}
func (v valueUnstructured) AsMap() Map {
return v.AsMapUsing(HeapAllocator)
}
func (v valueUnstructured) AsMapUsing(_ Allocator) Map {
if v.Value == nil {
panic("invalid nil")
}
switch t := v.Value.(type) {
case map[string]interface{}:
return mapUnstructuredString(t)
case map[interface{}]interface{}:
return mapUnstructuredInterface(t)
}
panic(fmt.Errorf("not a map: %#v", v))
}
func (v valueUnstructured) IsList() bool {
if v.Value == nil {
return false
}
_, ok := v.Value.([]interface{})
return ok
}
func (v valueUnstructured) AsList() List {
return v.AsListUsing(HeapAllocator)
}
func (v valueUnstructured) AsListUsing(_ Allocator) List {
return listUnstructured(v.Value.([]interface{}))
}
func (v valueUnstructured) IsFloat() bool {
if v.Value == nil {
return false
} else if _, ok := v.Value.(float64); ok {
return true
} else if _, ok := v.Value.(float32); ok {
return true
}
return false
}
func (v valueUnstructured) AsFloat() float64 {
if f, ok := v.Value.(float32); ok {
return float64(f)
}
return v.Value.(float64)
}
func (v valueUnstructured) IsInt() bool {
if v.Value == nil {
return false
} else if _, ok := v.Value.(int); ok {
return true
} else if _, ok := v.Value.(int8); ok {
return true
} else if _, ok := v.Value.(int16); ok {
return true
} else if _, ok := v.Value.(int32); ok {
return true
} else if _, ok := v.Value.(int64); ok {
return true
} else if _, ok := v.Value.(uint); ok {
return true
} else if _, ok := v.Value.(uint8); ok {
return true
} else if _, ok := v.Value.(uint16); ok {
return true
} else if _, ok := v.Value.(uint32); ok {
return true
}
return false
}
func (v valueUnstructured) AsInt() int64 {
if i, ok := v.Value.(int); ok {
return int64(i)
} else if i, ok := v.Value.(int8); ok {
return int64(i)
} else if i, ok := v.Value.(int16); ok {
return int64(i)
} else if i, ok := v.Value.(int32); ok {
return int64(i)
} else if i, ok := v.Value.(uint); ok {
return int64(i)
} else if i, ok := v.Value.(uint8); ok {
return int64(i)
} else if i, ok := v.Value.(uint16); ok {
return int64(i)
} else if i, ok := v.Value.(uint32); ok {
return int64(i)
}
return v.Value.(int64)
}
func (v valueUnstructured) IsString() bool {
if v.Value == nil {
return false
}
_, ok := v.Value.(string)
return ok
}
func (v valueUnstructured) AsString() string {
return v.Value.(string)
}
func (v valueUnstructured) IsBool() bool {
if v.Value == nil {
return false
}
_, ok := v.Value.(bool)
return ok
}
func (v valueUnstructured) AsBool() bool {
return v.Value.(bool)
}
func (v valueUnstructured) IsNull() bool {
return v.Value == nil
}
func (v valueUnstructured) Unstructured() interface{} {
return v.Value
}