gopatch

package module
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: May 10, 2020 License: MIT Imports: 6 Imported by: 0

README

Go PATCH

Project status Build Status Coverage Status GoDoc

Go PATCH is a structure patching package designed for receiving HTTP PATCH requests with a body of data, and applying it to a structure without affecting any other data in it.

Use the default patcher...

type User struct {

  ID           int     `json:"id"`
  Username     string  `json:"username"`
  EmailAddress string  `json:"email_address"`
  IsBanned     bool    `json:"is_banned"`
}

user := User{ Username: "Nifty255", EmailAddress: "really_old@address.com"}

results, err := gopatch.Default().Patch(user, map[string]interface{}{
  "EmailAddress": "shiny_new@address.com",
})

Or configure your own!

patcher := gopatch.NewPatcher(gopatch.PatcherConfig{
  PermittedFields: []string{ "username", "email_address" },
  UnpermittedErrors: true,
  PatchSource: "json",
})

// A nefarious user is trying to unban their own account.
nefariousPatchRequest := map[string]interface{}{
  "username": "an_inappropriate_word",
  "is_banned": false,
}

results, err := gopatch.Default().Patch(user, nefariousPatchRequest)

// err != nil

Using Go PATCH, structures can be patched by struct field name, or any name provided by any tag, including "json", and "bson". Furthermore, results are returned explaining which fields are patched, which fields weren't permitted, and even a map, sourced from the struct field names or any tag's names, which can be used to also patch a database representation.

Documentation

Overview

Package gopatch allows structures to be patched in a multitude of configurable ways. Patching is accomplished via Patchers, and a default is initialized for immediate use, found by calling `gopatch.Default()`. All initialized patchers can be used multiple times, and are thread safe.

Use the default patcher...

type User struct {

  ID           int     `json:"id"`
  Username     string  `json:"username"`
  EmailAddress string  `json:"email_address"`
  IsBanned     bool    `json:"is_banned"`
}

user := User{ Username: "Nifty255", EmailAddress: "really_old@address.com"}

results, err := gopatch.Default().Patch(user, map[string]interface{}{
  "EmailAddress": "shiny_new@address.com",
})

Or configure your own!

patcher := gopatch.NewPatcher(gopatch.PatcherConfig{
  PermittedFields: []string{ "username", "email_address" },
  UnpermittedErrors: true,
  PatchSource: "json",
})

// A nefarious user is trying to unban their own account.
nefariousPatchRequest := map[string]interface{}{
  "username": "an_inappropriate_word",
  "is_banned": false,
}

results, err := gopatch.Default().Patch(user, nefariousPatchRequest)

// err != nil

Some Limitations

Currently, gopatch cannot patch maps, and cannot replace maps not of the same key AND value types. Additionally, gopatch cannot patch or replace slices/arrays. Maps not of the same key and value types as well as all slices are currently skipped without error. However, it's easy to hook your own patch/replace logic by adding a custom Updater function to `gopatch.Updaters`. Note that these functions are run first to last, so you'll need to inject your function like so: `gopatch.Updaters = append(myUpdater, gopatch.Updaters...)`. Suggestions on how to remove these limitations are welcome. Please add an issue or make a pull request!

Field Name Sources

Struct field names on Go often differ from field names of counterpart object representations such as JSON and BSON. This package refers to these representations as "Field Name Sources". In the above examples, the User struct's EmailAddress has a "struct" field name source (resulting in the field name "EmailAddress"), and a "json" field name source (resulting in the field name "email_address"). Thus, if you want to patch a struct from a map built from JSON bytes, you need to add json tags AND configure your Patcher to use the struct's "json" field name sources. In the second example above, you can see the Patcher has been configured with the "json" field name source and thus can use a JSON-derived map to patch.

Patch Results

Patch operations return results or an error when they fail. These results can be used for purposes ranging from logging suspicious activity to persisting the changes to a database row or document. The results consist of an array of fields which were successfully patched, an array of fields which were found in the patch yet not permitted, and a map of the successfully updated fields and their values. Take the above example of patching a nefarious user's account. If `UnpermittedErrors` were false, the patch would succeed and result would not be nil; however, the Unpermitted array would contain "IsBanned", because the patching of that field wasn't permitted. Meanwhile, the "Fields" array would contain "Username" because it was permitted, and Map would contain the same data as `nefariousPatchRequest`, but without "is_banned".

Gopatch Field Tag

Patching behavior can be enforced while defining the structure by using the "gopatch" tag, which overrides configuration. This way, restrictions on how your database model can be patched can be limited while designing the model itself, rather than while designing the endpoint that patches it, reducing the chance of unexpected behavior.

type User struct {

  ID           int     `json:"id"        gopatch:"-"`        // NEVER patch this field, even if permitted in configuration.

  Username string      `json:"username"`                     // No gopatch tag allows normal patching behavior, acts like "patch" for structs.

  Profile  UserProfile `json:"profile"   gopatch:"patch"`    // Patch data for Profile will patch the fields inside.

  BanData  UserBanData `json:"ban_data"  gopatch:"replace"`  // Patch data for BanData will create a new zero-value BanData and patch that.
}

When the gopatch tag "patch" is used, the PatchResult's Map field will contain the struct field's values flattened with dot-notation keys created using the absolute path to the struct patched. For example, if the above User struct's `Profile.Motto` field is patched, the result's Map field would contain the following data: `"profile.motto": "..."`. This facilitates the patch-embedded-fields behavior of embedded objects in database servers such as MongoDB.

When the gopatch tag "replace" is used, the PatchResult's Map field will contain the struct field's values inside the struct field's patch key, exactly as presented to the Patcher. For example, if the above User struct's `BanData.Length` field is patched, the result's Map field would contain the following data: `"ban_data": map[string]interface{}{ "length": 30 }`. This facilitates the patch-whole-object behavior of embedded objects in database servers such as MongoDB.

Index

Constants

This section is empty.

Variables

Updaters is a collection of all default type updaters.

Functions

func BoolUpdater

func BoolUpdater(fieldValue reflect.Value, v reflect.Value) bool

BoolUpdater updates bool (pointer or value)

func FloatUpdater

func FloatUpdater(fieldValue reflect.Value, v reflect.Value) bool

FloatUpdater updates int (any float type Float8, Float16, Float32, Float64 and whether its a pointer or a value)

func IntUpdater

func IntUpdater(fieldValue reflect.Value, v reflect.Value) bool

IntUpdater updates int (any int type Int8, Int16, Int32, Int64 and whether its a pointer or a value)

func MapUpdater added in v0.0.3

func MapUpdater(fieldValue reflect.Value, v reflect.Value) bool

MapUpdater updates any map as long as the key and element values match.

func NullBoolUpdater

func NullBoolUpdater(fieldValue reflect.Value, v reflect.Value) bool

NullBoolUpdater updates null.Bool

func NullFloatUpdater

func NullFloatUpdater(fieldValue reflect.Value, v reflect.Value) bool

NullFloatUpdater updates null.Float64

func NullIntUpdater

func NullIntUpdater(fieldValue reflect.Value, v reflect.Value) bool

NullIntUpdater updates null.Int

func NullStringUpdater

func NullStringUpdater(fieldValue reflect.Value, v reflect.Value) bool

NullStringUpdater updates null.String

func NullTimeUpdater

func NullTimeUpdater(fieldValue reflect.Value, v reflect.Value) bool

NullTimeUpdater updates null.Time

func TimeUpdater

func TimeUpdater(fieldValue reflect.Value, v reflect.Value) bool

TimeUpdater updates time (pointer or value)

Types

type PatchResult

type PatchResult struct {

	// Fields is a lost of field names which were successfully patched. If
	// the Patcher's configured UpdatedFieldSource is empty or  "struct",
	// these names will be sourced from the struct. Otherwise, the Patcher
	// will attempt to find them from the field's tags related to the
	// configured source.
	Fields []string

	// Unpermitted is an array of fields in dot notation which were found to
	// be unpermitted for patching.
	Unpermitted []string

	// Map is a map of the successful patches made to the struct. This is
	// most useful as update data for a corresponding database row or
	// document. When a struct field is encountered and the field's
	// "gopatch" tag doesn't exist, is empty, or is set to "patch", that
	// field's patches are flattened into dot-notation in this map. If the
	// "gopatch" tag's value is set to "replace", the embedded struct will
	// be replaced with the patch data, with any unaccounted-for values
	// being initialized to their zero values.
	Map map[string]interface{}
}

PatchResult is the result of a patch operation. It contains both a list of fields patched (the values of which are affected by the Patcher's configuration) and a map of the patch performed, prepended with the Patcher's configured EmbedPath.

type Patcher

type Patcher struct {
	// contains filtered or unexported fields
}

Patcher is a configurable structure patcher.

func Default

func Default() Patcher

Default returns an instance of the default patcher.

func New added in v0.0.3

func New(config PatcherConfig) *Patcher

New creates a new Patcher instance with the specified configuration. See `patcher_config.go`.

func (Patcher) Patch

func (p Patcher) Patch(dest interface{}, patch map[string]interface{}) (*PatchResult, error)

Patch performs a patch operation on "dest", using the data in "patch". Patch returns a PatchResult if successful, or an error if not. Patch can also patch embedded structs and pointers to embedded structs. If a patch exists for a nil embedded struct pointer, the pointer will be assigned a new zero-value struct before it is patched.

type PatcherConfig

type PatcherConfig struct {

	// EmbedPath, if set, prepends PatchResult.Map's keys with the specified
	// string in dot notation. For example:
	//
	// // EmbedPath == "profile.metadata"
	//
	// updates, err := gopatch.Default()
	// .Patch(myUser, map[string]interface{}{
	//   "updated_at": "2020-01-01T00:00:00Z",
	// })
	//
	// // updates.Map == map[string]interface{}{
	// //   "profile.metadata.updated_at": "2020-01-01T00:00:00Z",
	// // }
	//
	// This is useful in the case the updated fields and their values should
	// also be applied to a database update operation.
	EmbedPath string

	// PatchSource, defaulting to "struct" when empty, determines from
	// which source a field name comes when matching a patch map's field
	// name to the struct. For "struct", the patch map's keys must match the
	// struct's field names. For any other value, the patcher will match
	// based on the struct fields' matching tags.
	//
	// An empty or "struct" value will use the field's Go-based name.
	// Use of any other value will cause the patcher to search for that tag.
	// Common values include "json", "bson", "msgpack", and "mapstructure"
	PatchSource string

	// PatchErrors causes the Patcher to immediately return an error if a
	// field is encountered which isn't tagged with the one passed to
	// PatchSource, and PatchSource is not empty or "struct". Defaults to
	// false.
	//
	// WARNING: Using this option may result in half-patched structures!
	// Only use this if you don't have further use of the half-patched
	// structure or can reload it afterwards.
	PatchErrors bool

	// UpdatedMapSource, defaulting to "struct" when empty, determines from
	// which source a field name comes when creating the PatchResult.Map
	// map. For "struct", the field string will be the name of the struct
	// field. For any other value, the patcher will look for the related
	// tag and use its value.
	//
	// An empty or "struct" value will use the field's Go-based name.
	// Use of any other value will cause the patcher to search for that tag.
	// Common values include "json", "bson", "msgpack", and "mapstructure"
	UpdatedMapSource string

	// UpdatedMapErrors causes the Patcher to immediately return an error
	// if a field is encountered which isn't tagged with the one passed to
	// UpdatedMapSource, and UpdatedMapSource is not empty or "struct".
	// Defaults to false.
	//
	// WARNING: Using this option may result in half-patched structures!
	// Only use this if you don't have further use of the half-patched
	// structure or can reload it afterwards.
	UpdatedMapErrors bool

	// UpdatedFieldSource, defaulting to "struct" when empty, determines
	// from which source a field name comes when creating the
	// PatchResult.Fields array. For "struct", the field string will be
	// the name of the struct field. For any other value, the patcher
	// will look for the related tag and use its value.
	//
	// An empty or "struct" value will use the field's Go-based name.
	// Use of any other value will cause the patcher to search for that tag.
	// Common values include "json", "bson", "msgpack", and "mapstructure"
	UpdatedFieldSource string

	// UpdatedFieldErrors causes the Patcher to immediately return an error
	// if a field is encountered which isn't tagged with the one passed to
	// UpdatedFieldSource, and UpdatedFieldSource is not empty or "struct".
	// Defaults to false.
	//
	// WARNING: Using this option may result in half-patched structures!
	// Only use this if you don't have further use of the half-patched
	// structure or can reload it afterwards.
	UpdatedFieldErrors bool

	// PermittedFields, if set, will prevent patches from fields in the
	// patch map that are not present in this array. For example:
	//
	// // PermittedFields == []string{"email_address"}
	//
	// updates, err := gopatch.Default()
	// .Patch(myUser, map[string]interface{}{
	//   "email_address": "myemail@address.com",
	//   "password_hash": "injectedhash"
	// })
	//
	// // updates.Fields == []string{"email_address"}
	// // updates.Unpermitted == []string{"password_hash"}
	//
	// To permit all fields of an embedded struct, use `embedded.*`. All
	// fields found to be unpermitted will be stored in dot notation in
	// the PatchResult's UnpermittedFields array if UnpermittedErrors is
	// false.
	PermittedFields []string

	// UnpermittedErrors causes the Patcher to immediately return an error
	// if a field is found to be unpermitted.
	//
	// WARNING: Using this option may result in half-patched structures!
	// Only use this if you don't have further use of the half-patched
	// structure or can reload it afterwards.
	UnpermittedErrors bool
}

PatcherConfig is the configuration object used to initialize new instances of Patcher.

Jump to

Keyboard shortcuts

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