Files
threshold/internal/libs/patchstruct/patchstruct.go
T
Chris Marchesi 86f2535aaf internal/libs/patchstruct: add package for patching structpb (#1592)
* internal/libs/patchstruct: add package for patching structpb

This adds a small helper package for patching *structpb.Struct values
using the same patterns that our APIs use for updating records. The
intent of this package is to provide a fallback mechanism for working
with these values when said record update behavior is unavailable, such
as when working with plugin-based host subtypes where the exact
structure of the subtype's attributes is unavailable.

patchstruct.Patch follows the following logic:

* The source (src) map is merged into the destination map (dst).
* Values are overwritten by the source map if they exist in both.
* Values are deleted from the destination if they are set to null in the
  source.
* Maps are recursively applied, meaning that a nested map at key "foo" in
  the destination would be patched with a map at key "foo" in the
  source.
* A map in the destination is overwritten by a non-map in the source,
  and a non-map in the destination is overwritten by a map in the
  source.

Patch returns the updated map as a copy, dst and src are not altered.

* Add doc comment for patchstruct.Patch

* vim snafu

* Premature commit of repository_host_catalog.go

* add nil cases

* internal/libs/patchstruct: add PatchJSON

* small comment formatting fix

* Change PatchJSON to PatchBytes

PatchJSON wasn't what was needed here - we need a helper to patch the
protobuf encoding of structpb.Struct.

* remove equality for original src/dst for patchbytes
2021-10-25 10:36:58 -07:00

91 lines
2.6 KiB
Go

package patchstruct
import (
"fmt"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
)
// PatchStruct updates the struct found in dst with the values found in src.
// The intent of this helper is to provide a fallback mechanism for subtype
// attributes when the actual schema of the subtype attributes are unknown. As
// such, it's preferred to use other methods (such as mask mapping) when an
// actual message for the subtype is known.
//
// The following rules apply:
//
// * The source (src) map is merged into the destination map (dst).
//
// * Values are overwritten by the source map if they exist in both.
//
// * Values are deleted from the destination if they are set to null in the
// source.
//
// * Maps are recursively applied, meaning that a nested map at key "foo" in
// the destination would be patched with a map at key "foo" in the
// source.
//
// * A map in the destination is overwritten by a non-map in the source,
// and a non-map in the destination is overwritten by a map in the
// source.
//
// PatchStruct returns the updated map as a copy, dst and src are not altered.
func PatchStruct(dst, src *structpb.Struct) *structpb.Struct {
result, err := structpb.NewStruct(patchM(dst.AsMap(), src.AsMap()))
if err != nil {
// Should never error as values are source from structpb values
panic(err)
}
return result
}
// PatchBytes follows the same rules as above with PatchStruct, but
// instead of patching structpb.Structs, it patches the protobuf
// encoding. An error is returned if there are issues working with
// the data.
func PatchBytes(dst, src []byte) ([]byte, error) {
srcpb, dstpb := new(structpb.Struct), new(structpb.Struct)
if err := proto.Unmarshal(dst, dstpb); err != nil {
return nil, fmt.Errorf("error reading destination data: %w", err)
}
if err := proto.Unmarshal(src, srcpb); err != nil {
return nil, fmt.Errorf("error reading source data: %w", err)
}
result, err := proto.Marshal(PatchStruct(dstpb, srcpb))
if err != nil {
return nil, fmt.Errorf("error writing result data: %w", err)
}
return result, nil
}
func patchM(dst, src map[string]interface{}) map[string]interface{} {
for k, v := range src {
switch x := v.(type) {
case map[string]interface{}:
if y, ok := dst[k].(map[string]interface{}); ok {
// If the value in dst a map, continue to patch
dst[k] = patchM(y, x)
} else {
// Overwrite
dst[k] = x
}
default:
if v == nil {
// explicit null values delete values at that key in dst
delete(dst, k)
} else {
// Anything else gets overwritten
dst[k] = v
}
}
}
return dst
}