fieldmask_utils

package module
v1.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 14, 2024 License: MIT Imports: 7 Imported by: 20

README

Protobuf Field Mask utils for Go

Tests Coverage

Features:

  • Copy from any Go struct to any compatible Go struct with a field mask applied
  • Copy from any Go struct to a map[string]interface{} with a field mask applied
  • Extensible masks (e.g. inverse mask: copy all except those mentioned, etc.)
  • Supports Protobuf Any message types.

If you're looking for a simple FieldMask library to work with protobuf messages only (not arbitrary structs) consider this tiny repo: https://github.com/mennanov/fmutils

Examples

Copy from a protobuf message to a protobuf message:

// testproto/test.proto

message UpdateUserRequest {
    User user = 1;
    google.protobuf.FieldMask field_mask = 2;
}
package main

import fieldmask_utils "github.com/mennanov/fieldmask-utils"

// A function that maps field mask field names to the names used in Go structs.
// It has to be implemented according to your needs.
// Scroll down for a reference on how to apply field masks to your gRPC services.
func naming(s string) string {
	if s == "foo" {
		return "Foo"
	}
	return s
}

func main () {
	var request UpdateUserRequest
	userDst := &testproto.User{} // a struct to copy to
	mask, _ := fieldmask_utils.MaskFromPaths(request.FieldMask.Paths, naming)
	fieldmask_utils.StructToStruct(mask, request.User, userDst)
	// Only the fields mentioned in the field mask will be copied to userDst, other fields are left intact
}

Copy from a protobuf message to a map[string]interface{}:

package main

import fieldmask_utils "github.com/mennanov/fieldmask-utils"

func main() {
	var request UpdateUserRequest
	userDst := make(map[string]interface{}) // a map to copy to
	mask, _ := fieldmask_utils.MaskFromProtoFieldMask(request.FieldMask, naming)
	err := fieldmask_utils.StructToMap(mask, request.User, userDst)
	// Only the fields mentioned in the field mask will be copied to userDst, other fields are left intact
}

Copy with an inverse mask:

package main

import fieldmask_utils "github.com/mennanov/fieldmask-utils"

func main() {
	var request UpdateUserRequest
	userDst := &testproto.User{} // a struct to copy to
	mask := fieldmask_utils.MaskInverse{"Id": nil, "Friends": fieldmask_utils.MaskInverse{"Username": nil}}
	fieldmask_utils.StructToStruct(mask, request.User, userDst)
	// Only the fields that are not mentioned in the field mask will be copied to userDst, other fields are left intact.
}
Naming function

For developers that are looking for a mechanism to apply a mask field in their update endpoints using gRPC services, there are multiple options for the naming function described above:

  • Using the CamelCase function provided in the original protobuf repository. This repository has been deprecated and it will potentially trigger lint errors.
    • You can copy-paste the CamelCase function to your own project or,
    • You can use an Open Source alternative that provides the same functionality, already took care of copying the code, and also added tests.
func main() {
    mask := &fieldmaskpb.FieldMask{Paths: []string{"username"}}
    mask.Normalize()
    req := &UpdateUserRequest{
        User: &User{
            Id:       1234,
            Username: "Test",
        },
    }
    if !mask.IsValid(req) {
        return
    }
    protoMask, err := fieldmask_utils.MaskFromProtoFieldMask(mask, strings.PascalCase)
    if err != nil {
        return
    }
    m := make(map[string]any)
    err = fieldmask_utils.StructToMap(protoMask, req, m)
	if err != nil {
		return
    }
	fmt.Println("Resulting map:", m)
}

This will result in a map that contains the fields that need to be updated with their respective values.

Limitations

  1. Larger scope field masks have no effect and are not considered invalid:

    field mask strings "a", "a.b", "a.b.c" will result in a mask a{b{c}}, which is the same as "a.b.c".

  2. Masks inside a protobuf Map are not supported.

  3. When copying from a struct to struct the destination struct must have the same fields (or a subset) as the source struct. Either of source or destination fields can be a pointer as long as it is a pointer to the type of the corresponding field.

  4. oneof fields are represented differently in fieldmaskpb.FieldMask compared to fieldmask_util.Mask. In FieldMask the fields are represented using their property name, in this library they are prefixed with the oneof name matching how Go generated code is laid out. This can lead to issues when converting between the two, for example when using MaskFromPaths or MaskFromProtoFieldMask.

Documentation

Overview

Package fieldmask_utils provides utility functions for copying data from structs using a field mask.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func StructToMap

func StructToMap(filter FieldFilter, src interface{}, dst map[string]interface{}, userOpts ...Option) error

StructToMap copies `src` struct to the `dst` map. Behavior is similar to `StructToStruct`. Arrays in the non-empty dst are converted to slices.

func StructToStruct

func StructToStruct(filter FieldFilter, src, dst interface{}, userOpts ...Option) error

StructToStruct copies `src` struct to `dst` struct using the given FieldFilter. Only the fields where FieldFilter returns true will be copied to `dst`. `src` and `dst` must be coherent in terms of the field names, but it is not required for them to be of the same type. Unexported fields are copied only if the corresponding struct filter is empty and `dst` is assignable to `src`.

Types

type FieldFilter

type FieldFilter interface {
	// Filter should return a corresponding FieldFilter for the given fieldName and a boolean result. If result is true
	// then the field is copied, skipped otherwise.
	Filter(fieldName string) (FieldFilter, bool)
	// Returns true if the FieldFilter is empty. In this case all fields are copied.
	IsEmpty() bool
}

FieldFilter is an interface used by the copying function to filter fields that are needed to be copied.

type FieldFilterContainer

type FieldFilterContainer interface {
	FieldFilter
	// Get gets the FieldFilter for the given field name. Result is false if the filter is not found.
	Get(fieldName string) (filter FieldFilterContainer, result bool)
	// Set sets the FieldFilter for the given field name.
	Set(fieldName string, filter FieldFilterContainer)
}

FieldFilterContainer is a FieldFilter with additional methods Get and Set.

func FieldFilterFromPaths

func FieldFilterFromPaths(paths []string, naming func(string) string, filter func() FieldFilterContainer) (FieldFilterContainer, error)

FieldFilterFromPaths creates a new FieldFilter from the given paths.

func FieldFilterFromString

func FieldFilterFromString(input string, filter func() FieldFilterContainer) FieldFilterContainer

FieldFilterFromString creates a new FieldFilterContainer from string. Input string is supposed to be a valid string representation of a FieldFilter like "a,b,c{d,e{f,g}},d". Use it in tests only as the input string is not validated and the underlying function panics in case of a parse error.

type MapVisitorResult added in v1.0.0

type MapVisitorResult struct {
	SkipToNext bool
	UpdatedDst *reflect.Value
}

type Mask

Mask is a tree-based implementation of a FieldFilter.

func MaskFromPaths

func MaskFromPaths(paths []string, naming func(string) string) (Mask, error)

MaskFromPaths creates a new Mask from the given paths.

func MaskFromProtoFieldMask

func MaskFromProtoFieldMask(fm *field_mask.FieldMask, naming func(string) string) (Mask, error)

MaskFromProtoFieldMask creates a Mask from the given FieldMask.

func MaskFromString

func MaskFromString(s string) Mask

MaskFromString creates a new Mask instance from a given string. Use in tests only. See FieldFilterFromString for details.

func (Mask) Filter

func (m Mask) Filter(fieldName string) (FieldFilter, bool)

Filter returns true for those fieldNames that exist in the underlying map. Field names that start with "XXX_" are ignored as unexported.

func (Mask) Get

func (m Mask) Get(fieldName string) (FieldFilterContainer, bool)

Get gets the FieldFilter for the given field name. Result is false if the filter is not found.

func (Mask) IsEmpty

func (m Mask) IsEmpty() bool

IsEmpty returns true of the mask is empty.

func (Mask) Set

func (m Mask) Set(fieldName string, filter FieldFilterContainer)

Set sets the FieldFilter for the given field name.

func (Mask) String

func (m Mask) String() string

type MaskInverse

type MaskInverse map[string]FieldFilterContainer

MaskInverse is an inversed version of a Mask (will copy all the fields except those mentioned in the mask).

func MaskInverseFromPaths

func MaskInverseFromPaths(paths []string, naming func(string) string) (MaskInverse, error)

MaskInverseFromPaths creates a new MaskInverse from the given paths.

func MaskInverseFromProtoFieldMask

func MaskInverseFromProtoFieldMask(fm *field_mask.FieldMask, naming func(string) string) (MaskInverse, error)

MaskInverseFromProtoFieldMask creates a MaskInverse from the given FieldMask.

func MaskInverseFromString

func MaskInverseFromString(s string) MaskInverse

MaskInverseFromString creates a new MaskInverse instance from a given string. Use in tests only. See FieldFilterFromString for details.

func (MaskInverse) Filter

func (m MaskInverse) Filter(fieldName string) (FieldFilter, bool)

Filter returns true for those fieldNames that do NOT exist in the underlying map. Field names that start with "XXX_" are ignored as unexported.

func (MaskInverse) Get

func (m MaskInverse) Get(fieldName string) (FieldFilterContainer, bool)

Get gets the FieldFilter for the given field name. Result is false if the filter is not found.

func (MaskInverse) IsEmpty

func (m MaskInverse) IsEmpty() bool

IsEmpty returns true if the mask is empty.

func (MaskInverse) Set

func (m MaskInverse) Set(fieldName string, filter FieldFilterContainer)

Set sets the FieldFilter for the given field name.

func (MaskInverse) String

func (m MaskInverse) String() string

type Option added in v0.3.0

type Option func(*options)

Option function modifies the given options.

func WithCopyListSize added in v0.6.0

func WithCopyListSize(f func(src *reflect.Value) int) Option

WithCopyListSize sets CopyListSize func you can set copy size according to src.

func WithMapVisitor added in v0.7.0

func WithMapVisitor(visitor mapVisitor) Option

WithMapVisitor sets the fields visitor function for StructToMap.

func WithSrcTag added in v1.1.0

func WithSrcTag(s string) Option

WithSrcTag sets an option that gets the source field name from the field's tag.

func WithTag added in v0.3.0

func WithTag(s string) Option

WithTag sets the destination field name

func WithUnmarshalAllAny added in v1.1.2

func WithUnmarshalAllAny(unmarshal bool) Option

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL