Skip to content

Commit

Permalink
switch to 'sigs.k8s.io/json' and 'gopkg.in/yaml.v3' & update interface
Browse files Browse the repository at this point in the history
Signed-off-by: Inteon <42113979+inteon@users.noreply.github.com>
  • Loading branch information
inteon committed Nov 3, 2021
1 parent 7899e89 commit 546762b
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 361 deletions.
6 changes: 1 addition & 5 deletions fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ func indirect(value reflect.Value, decodingNull bool) (json.Unmarshaler, encodin
break
}
if value.IsNil() {
if value.CanSet() {
value.Set(reflect.New(value.Type().Elem()))
} else {
value = reflect.New(value.Type().Elem())
}
value = reflect.New(value.Type().Elem())
}
if value.Type().NumMethod() > 0 {
if u, ok := value.Interface().(json.Unmarshaler); ok {
Expand Down
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
module sigs.k8s.io/yaml

go 1.12
go 1.17

require (
github.com/davecgh/go-spew v1.1.1
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6
)

replace gopkg.in/yaml.v3 => github.com/amurant/go-yaml v0.0.0-20211031110346-baaeb5da4f5b
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/amurant/go-yaml v0.0.0-20211031110346-baaeb5da4f5b h1:oFp8AMYmnVvsTyGQR4dEKki4JxRHpxUURat0tqhfl8s=
github.com/amurant/go-yaml v0.0.0-20211031110346-baaeb5da4f5b/go.mod h1:S0zpg6BiGH33bJz4jI0zkXVdP/pvBisCvtLNJ/AWKZw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s=
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
214 changes: 87 additions & 127 deletions yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,24 @@ package yaml
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"strconv"

"gopkg.in/yaml.v2"
kubejson "sigs.k8s.io/json"

"gopkg.in/yaml.v3"
)

type yamlToTargetOption uint8

const (
// no options enabled
none yamlToTargetOption = 0

// disallowUnknownFields returns strict errors if data contains unknown fields when decoding into typed structs
disallowUnknownFields yamlToTargetOption = 1 << iota
)

// Marshal marshals obj into JSON using stdlib json.Marshal, and then converts JSON to YAML using JSONToYAML (see that method for more reference)
Expand All @@ -37,79 +49,81 @@ func Marshal(obj interface{}) ([]byte, error) {
return JSONToYAML(jsonBytes)
}

// JSONOpt is a decoding option for decoding from JSON format.
type JSONOpt func(*json.Decoder) *json.Decoder

// Unmarshal first converts the given YAML to JSON, and then unmarshals the JSON into obj. Options for the
// standard library json.Decoder can be optionally specified, e.g. to decode untyped numbers into json.Number instead of float64, or to disallow unknown fields (but for that purpose, see also UnmarshalStrict). obj must be a non-nil pointer.
//
// Important notes about the Unmarshal logic:
//
// - Decoding is case-insensitive, unlike the rest of Kubernetes API machinery, as this is using the stdlib json library. This might be confusing to users.
// - This decodes any number (although it is an integer) into a float64 if the type of obj is unknown, e.g. *map[string]interface{}, *interface{}, or *[]interface{}. This means integers above +/- 2^53 will lose precision when round-tripping. Make a JSONOpt that calls d.UseNumber() to avoid this.
// - Duplicate fields, including in-case-sensitive matches, are ignored in an undefined order. Note that the YAML specification forbids duplicate fields, so this logic is more permissive than it needs to. See UnmarshalStrict for an alternative.
// - Unknown fields, i.e. serialized data that do not map to a field in obj, are ignored. Use d.DisallowUnknownFields() or UnmarshalStrict to override.
// - As per the YAML 1.1 specification, which yaml.v2 used underneath implements, literal 'yes' and 'no' strings without quotation marks will be converted to true/false implicitly.
// - Decoding is sensitive
// - Duplicate fields (only case-sensitive matches), result in a fatal error
// - Unknown fields, i.e. serialized data that do not map to a field in obj, are ignored. Use UnmarshalStrict to override.
// - YAML non-string keys, e.g. ints, bools and floats, are converted to strings implicitly during the YAML to JSON conversion process.
// - There are no compatibility guarantees for returned error values.
func Unmarshal(yamlBytes []byte, obj interface{}, opts ...JSONOpt) error {
return unmarshal(yamlBytes, obj, yaml.Unmarshal, opts...)
func Unmarshal(yamlBytes []byte, obj interface{}) error {
_, err := unmarshal(yamlBytes, obj, none)
return err
}

// UnmarshalStrict is similar to Unmarshal (please read its documentation for reference), with the following exceptions:
//
// - Duplicate fields in an object yield an error. This is according to the YAML specification.
// - If obj, or any of its recursive children, is a struct, presence of fields in the serialized data unknown to the struct will yield an error.
func UnmarshalStrict(yamlBytes []byte, obj interface{}, opts ...JSONOpt) error {
return unmarshal(yamlBytes, obj, yaml.UnmarshalStrict, append(opts, DisallowUnknownFields)...)
// - If obj, or any of its recursive children, is a struct, presence of fields in the serialized data unknown to the struct will yield a strict error.
func UnmarshalStrict(yamlBytes []byte, obj interface{}) (strictErrors []error, err error) {
return unmarshal(yamlBytes, obj, disallowUnknownFields)
}

// unmarshal unmarshals the given YAML byte stream into the given interface,
// optionally performing the unmarshalling strictly
func unmarshal(yamlBytes []byte, obj interface{}, unmarshalFn func([]byte, interface{}) error, opts ...JSONOpt) error {
func unmarshal(yamlBytes []byte, obj interface{}, options yamlToTargetOption) (strictErrors []error, err error) {
jsonTarget := reflect.ValueOf(obj)
if jsonTarget.Kind() != reflect.Ptr || jsonTarget.IsNil() {
return nil, fmt.Errorf("provided object is not a valid pointer")
}

jsonBytes, err := yamlToJSONTarget(yamlBytes, &jsonTarget, unmarshalFn)
jsonBytes, strictErrors, err := yamlToJSONTarget(yamlBytes, &jsonTarget, options)
if err != nil {
return fmt.Errorf("error converting YAML to JSON: %w", err)
return strictErrors, err
}

err = jsonUnmarshal(bytes.NewReader(jsonBytes), obj, opts...)
// Decode jsonBytes into obj.
strictOptions := []kubejson.StrictOption{}
if options&disallowUnknownFields != 0 {
strictOptions = append(strictOptions, kubejson.DisallowUnknownFields)
}
strictErrors2, err := kubejson.UnmarshalStrict(jsonBytes, &obj, strictOptions...)
strictErrors = append(strictErrors, strictErrors2...)
if err != nil {
return fmt.Errorf("error unmarshaling JSON: %w", err)
return strictErrors, fmt.Errorf("error unmarshaling JSON: %w", err)
}

return nil
return strictErrors, nil
}

// jsonUnmarshal unmarshals the JSON byte stream from the given reader into the
// object, optionally applying decoder options prior to decoding. We are not
// using json.Unmarshal directly as we want the chance to pass in non-default
// options.
func jsonUnmarshal(reader io.Reader, obj interface{}, opts ...JSONOpt) error {
d := json.NewDecoder(reader)
for _, opt := range opts {
d = opt(d)
func yamlUnmarshal(yamlBytes []byte, v interface{}, options yamlToTargetOption) (strictErrors []error, err error) {
dec := yaml.NewDecoder(bytes.NewReader(yamlBytes))
dec.KnownFields(options&disallowUnknownFields != 0)
decodeError := dec.Decode(v)
if e, ok := decodeError.(*yaml.TypeError); ok {
strictErrors := make([]error, 0, len(e.StrictErrors))
for _, err := range e.StrictErrors {
strictErrors = append(strictErrors, errors.New(err))
}
if len(e.Errors) == 0 {
return strictErrors, nil
}

return strictErrors, &yaml.TypeError{Errors: e.Errors}
}
return d.Decode(obj)
return nil, decodeError
}

// JSONToYAML converts JSON to YAML. Notable implementation details:
//
// - Duplicate fields, are case-sensitively ignored in an undefined order.
// - The sequence indentation style is compact, which means that the "- " marker for a YAML sequence will be on the same indentation level as the sequence field name.
// - Unlike Unmarshal, all integers, up to 64 bits, are preserved during this round-trip.
func JSONToYAML(j []byte) ([]byte, error) {
// - Duplicate fields (only case-sensitive matches), result in a fatal error
func JSONToYAML(jsonBytes []byte) ([]byte, error) {
// Convert the JSON to an object.
var jsonObj interface{}

// We are using yaml.Unmarshal here (instead of json.Unmarshal) because the
// Go JSON library doesn't try to pick the right number type (int, float,
// etc.) when unmarshalling to interface{}, it just picks float64
// universally. go-yaml does go through the effort of picking the right
// number type, so we can preserve number type throughout this process.
err := yaml.Unmarshal(j, &jsonObj)
if err != nil {
if err := kubejson.UnmarshalCaseSensitivePreserveInts(jsonBytes, &jsonObj); err != nil {
return nil, fmt.Errorf("error converting JSON to YAML: %w", err)
}

Expand All @@ -133,29 +147,17 @@ func JSONToYAML(j []byte) ([]byte, error) {
// not use the !!binary tag in your YAML. This will ensure the original base64
// encoded data makes it all the way through to the JSON.
// * And more... read the YAML specification for more details.
//
// Notable about the implementation:
//
// - Duplicate fields are case-sensitively ignored in an undefined order. Note that the YAML specification forbids duplicate fields, so this logic is more permissive than it needs to. See YAMLToJSONStrict for an alternative.
// - As per the YAML 1.1 specification, which yaml.v2 used underneath implements, literal 'yes' and 'no' strings without quotation marks will be converted to true/false implicitly.
// - Unlike Unmarshal, all integers, up to 64 bits, are preserved during this round-trip.
// - There are no compatibility guarantees for returned error values.
func YAMLToJSON(y []byte) ([]byte, error) {
return yamlToJSONTarget(y, nil, yaml.Unmarshal)
}

// YAMLToJSONStrict is like YAMLToJSON but enables strict YAML decoding,
// returning an error on any duplicate field names.
func YAMLToJSONStrict(y []byte) ([]byte, error) {
return yamlToJSONTarget(y, nil, yaml.UnmarshalStrict)
func YAMLToJSON(yamlBytes []byte) ([]byte, error) {
json, _, err := yamlToJSONTarget(yamlBytes, nil, none)
return json, err
}

func yamlToJSONTarget(yamlBytes []byte, jsonTarget *reflect.Value, unmarshalFn func([]byte, interface{}) error) ([]byte, error) {
func yamlToJSONTarget(yamlBytes []byte, jsonTarget *reflect.Value, options yamlToTargetOption) (jsonBytes []byte, strictErrors []error, err error) {
// Convert the YAML to an object.
var yamlObj interface{}
err := unmarshalFn(yamlBytes, &yamlObj)
strictErrors, err = yamlUnmarshal(yamlBytes, &yamlObj, options)
if err != nil {
return nil, fmt.Errorf("error converting YAML to JSON: %w", err)
return nil, strictErrors, fmt.Errorf("error converting YAML to JSON: %w", err)
}

// YAML objects are not completely compatible with JSON objects (e.g. you
Expand All @@ -164,15 +166,15 @@ func yamlToJSONTarget(yamlBytes []byte, jsonTarget *reflect.Value, unmarshalFn f
// incompatibilties happen along the way.
jsonObj, err := convertToJSONableObject(yamlObj, jsonTarget)
if err != nil {
return nil, fmt.Errorf("error converting YAML to JSON: %w", err)
return nil, strictErrors, fmt.Errorf("error converting YAML to JSON: %w", err)
}

// Convert this object to JSON and return the data.
jsonBytes, err := json.Marshal(jsonObj)
jsonBytes, err = json.Marshal(jsonObj)
if err != nil {
return nil, fmt.Errorf("error converting YAML to JSON: %w", err)
return nil, strictErrors, fmt.Errorf("error converting YAML to JSON: %w", err)
}
return jsonBytes, nil
return jsonBytes, strictErrors, nil
}

func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (interface{}, error) {
Expand All @@ -198,7 +200,15 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
// If yamlObj is a map or array, find the field that each key is
// unmarshaling to, and when you recurse pass the reflect.Value for that
// field back into this function.
cast_type:
switch typedYAMLObj := yamlObj.(type) {
case map[string]interface{}:
temp := make(map[interface{}]interface{})
for k, v := range typedYAMLObj {
temp[k] = v
}
yamlObj = temp
goto cast_type
case map[interface{}]interface{}:
// JSON does not support arbitrary keys in a map, so we must convert
// these keys to strings.
Expand Down Expand Up @@ -271,7 +281,17 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
if f != nil {
// Find the reflect.Value of the most preferential
// struct field.
jtf := t.Field(f.index[0])
jtf := t
for _, i := range f.index {
if jtf.Kind() == reflect.Ptr {
if jtf.IsNil() {
jtf = reflect.New(jtf.Type().Elem())
}
jtf = jtf.Elem()
}
jtf = jtf.Field(i)
}

strMap[keyString], err = convertToJSONableObject(v, &jtf)
if err != nil {
return nil, err
Expand Down Expand Up @@ -336,7 +356,7 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
case int64:
s = strconv.FormatInt(typedVal, 10)
case float64:
s = strconv.FormatFloat(typedVal, 'g', -1, 32)
s = strconv.FormatFloat(typedVal, 'g', -1, 64)
case uint64:
s = strconv.FormatUint(typedVal, 10)
case bool:
Expand All @@ -350,67 +370,7 @@ func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (in
yamlObj = interface{}(s)
}
}
return yamlObj, nil
}
}

// JSONObjectToYAMLObject converts an in-memory JSON object into a YAML in-memory MapSlice,
// without going through a byte representation. A nil or empty map[string]interface{} input is
// converted to an empty map, i.e. yaml.MapSlice(nil).
//
// interface{} slices stay interface{} slices. map[string]interface{} becomes yaml.MapSlice.
//
// int64 and float64 are down casted following the logic of github.com/go-yaml/yaml:
// - float64s are down-casted as far as possible without data-loss to int, int64, uint64.
// - int64s are down-casted to int if possible without data-loss.
//
// Big int/int64/uint64 do not lose precision as in the json-yaml roundtripping case.
//
// string, bool and any other types are unchanged.
func JSONObjectToYAMLObject(j map[string]interface{}) yaml.MapSlice {
if len(j) == 0 {
return nil
}
ret := make(yaml.MapSlice, 0, len(j))
for k, v := range j {
ret = append(ret, yaml.MapItem{Key: k, Value: jsonToYAMLValue(v)})
}
return ret
}

func jsonToYAMLValue(j interface{}) interface{} {
switch j := j.(type) {
case map[string]interface{}:
if j == nil {
return interface{}(nil)
}
return JSONObjectToYAMLObject(j)
case []interface{}:
if j == nil {
return interface{}(nil)
}
ret := make([]interface{}, len(j))
for i := range j {
ret[i] = jsonToYAMLValue(j[i])
}
return ret
case float64:
// replicate the logic in https://github.com/go-yaml/yaml/blob/51d6538a90f86fe93ac480b35f37b2be17fef232/resolve.go#L151
if i64 := int64(j); j == float64(i64) {
if i := int(i64); i64 == int64(i) {
return i
}
return i64
}
if ui64 := uint64(j); j == float64(ui64) {
return ui64
}
return j
case int64:
if i := int(j); j == int64(i) {
return i
}
return j
return yamlObj, nil
}
return j
}
31 changes: 0 additions & 31 deletions yaml_go110.go

This file was deleted.

0 comments on commit 546762b

Please sign in to comment.