Create a gist now

Instantly share code, notes, and snippets.

@bolshoy /partial.go
Last active Aug 29, 2015

What would you like to do?
package partial
import (
"encoding/json"
"fmt"
"reflect"
)
// OpType defines possible CRUD operations.
type OpType int
// OpType enumeration.
const (
CreateOp OpType = iota
UpdateOp
DeleteOp
)
// KeyNotFoundError is returned when trying to access a missing key.
type KeyNotFoundError struct {
key string
}
// Error implements error interface, stating that key was not found.
func (e *KeyNotFoundError) Error() string {
return "key not found: " + e.key
}
// Key returns the key that was missing.
func (e *KeyNotFoundError) Key() string {
return e.key
}
// KeyExistsError is returned when creating fails due to existing key.
type KeyExistsError struct {
key string
}
// Error implements error interface, stating that key already exists.
func (e *KeyExistsError) Error() string {
return "key exists: " + e.key
}
// Key returns the key that was duplicate.
func (e *KeyExistsError) Key() string {
return e.key
}
// SelectData will try to extract a part of dataIn identified by path elements.
func SelectData(dataIn interface{}, path []string) (partOut interface{}, err error) {
// Return myself if there's no subpath anymore (recursion termination).
if len(path) == 0 {
partOut = dataIn
return
}
subPath := path[0]
typ := reflect.TypeOf(dataIn)
val := reflect.ValueOf(dataIn)
// There's a subpath, so we need to drill down. We can only drill down into a map or a struct,
// because they have named fields/keys.
switch typ.Kind() {
case reflect.Map:
// Get the value from the map keyed by the first path element.
mapVal := val.MapIndex(reflect.ValueOf(subPath))
if mapVal.IsValid() { // value found - subpath matched
return SelectData(mapVal.Interface(), path[1:])
}
case reflect.Struct:
// Iterate over object fields and see if there's a field whose json tag matches
// the first element in the path.
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
tag := field.Tag.Get("json") // get json tag of this field
if tag == subPath { // subpath matched
return SelectData(val.Field(i).Interface(), path[1:])
}
}
case reflect.Ptr, reflect.Interface:
if !val.Elem().IsValid() { // cannot traverse further
err = fmt.Errorf("field referenced by [%s] is invalid pointer/interface",
subPath)
return
}
return SelectData(val.Elem().Interface(), path) // dereference and call recursively
default:
err = fmt.Errorf("field referenced by [%s] must be struct/map, %s given",
subPath, typ.Kind())
return
}
// subPath was not matched or could not traverse into the corresponding field.
err = &KeyNotFoundError{subPath}
return
}
// ModifyDataJSON will try to perform the operation identified by opType on the source object dataIn,
// given traversal path and a json for a new/updated object.
func ModifyDataJSON(dataIn interface{}, path []string, dataJSON string, opType OpType) (err error) {
ptrSrcVal := reflect.ValueOf(dataIn)
srcVal := reflect.Indirect(ptrSrcVal)
srcValType := srcVal.Type()
// Update myself if there's no subpath (recursion termination).
if len(path) == 0 {
// Create a concrete object to unmarshal to.
dstVal := reflect.New(srcValType)
// Try to unmarshal.
err = json.Unmarshal([]byte(dataJSON), dstVal.Interface())
if err != nil {
return err
}
// Unmarshalled successfully, update the source object.
srcVal.Set(dstVal.Elem())
return nil
}
// There's a subpath, see if we can drill down.
subPath := path[0]
subPathVal := reflect.ValueOf(subPath)
// Identify the field indexed by path[0] and try to drill down into it.
// We can drill down into a struct, a map or a pointer/interface to them.
switch srcValType.Kind() {
case reflect.Map:
if srcVal.IsNil() { // uninited map
if opType == DeleteOp {
// Cannot delete, key not found
return &KeyNotFoundError{subPath}
}
// Otherwise, create an empty map and continue.
srcVal.Set(reflect.MakeMap(srcValType))
}
// Check if the first path element exists as a key in this map.
mapVal := srcVal.MapIndex(subPathVal)
if mapVal.IsValid() { // key exists in map
if len(path) == 1 { // last element in the path
if opType == CreateOp {
// We cannot create an existing key.
return &KeyExistsError{subPath}
} else if opType == DeleteOp {
// Alright, delete the entry and leave.
srcVal.SetMapIndex(subPathVal, reflect.Value{})
return nil
}
}
// Otherwise see if we can drill into the value.
elKind := reflect.Indirect(mapVal).Kind()
if isTraversable(elKind) {
// Drill down and update mapVal recursively.
err := ModifyDataJSON(mapVal.Interface(), path[1:], dataJSON, opType)
if err != nil {
return err
}
// Alright, update the original map and leave.
srcVal.SetMapIndex(subPathVal, mapVal) // TODO: needed?
return nil
}
} else { // no such key in map
if len(path) == 1 { // last element in the path
// On this stage, we can only create a new map entry. Updating and
// deleting will cause KeyNotFoundError.
if opType == CreateOp {
elType := srcValType.Elem()
// Create a new map element.
mapVal := reflect.New(elType.Elem())
// Create a map if needed.
if elType.Kind() == reflect.Map {
mapVal.Set(reflect.MakeMap(elType))
}
// Update the newly created element.
err := ModifyDataJSON(mapVal.Interface(), path[1:],
dataJSON, opType)
if err != nil {
return err
}
// Alright, update the original map with the new element.
srcVal.SetMapIndex(subPathVal, mapVal)
return nil
}
}
}
case reflect.Struct:
// Iterate over object fields and see if there's a field whose json tag matches
// the first element in the path.
for i := 0; i < srcValType.NumField(); i++ {
field := srcValType.Field(i)
fieldKind := field.Type.Kind()
tag := field.Tag.Get("json")
if tag == "" {
tag = field.Name // if no json tag use the field name
}
if tag == subPath { // matches the first path element
if len(path) == 1 { // last element in the path
if opType == CreateOp {
// Return error because we cannot create a struct field.
return &KeyExistsError{subPath}
} else if opType == DeleteOp {
// Return error because we cannot delete a struct field.
return fmt.Errorf("cannot delete a struct field")
}
}
// Otherwise see if we can drill into the value.
if isTraversable(fieldKind) {
return ModifyDataJSON(srcVal.Field(i).Addr().Interface(),
path[1:], dataJSON, opType)
}
}
}
case reflect.Ptr, reflect.Interface:
if !srcVal.Elem().IsValid() { // not yet initialized, cannot traverse
if len(path) == 1 { // last element in the path
// On this stage, we can only create a new entry. Updating and deleting
// will cause KeyNotFoundError.
if opType == CreateOp {
srcVal.Set(reflect.New(srcValType.Elem()))
return ModifyDataJSON(srcVal.Elem().Addr().Interface(),
path[1:], dataJSON, opType) // call recursively
}
}
} else {
return ModifyDataJSON(srcVal.Elem().Addr().Interface(), path[1:],
dataJSON, opType) // call recursively
}
default:
return fmt.Errorf("field referenced by [%s] must be traversable, %s given",
subPath, srcValType.Kind())
}
return &KeyNotFoundError{subPath}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment