Last active
August 29, 2015 14:21
-
-
Save bolshoy/573fa17e787aeca9057b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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