123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- // Copyright 2012-2014, 2017 Charles Banning. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file
- // updatevalues.go - modify a value based on path and possibly sub-keys
- // TODO(clb): handle simple elements with attributes and NewMapXmlSeq Map values.
- package mxj
- import (
- "fmt"
- "strconv"
- "strings"
- )
- // Update value based on path and possible sub-key values.
- // A count of the number of values changed and any error are returned.
- // If the count == 0, then no path (and subkeys) matched.
- // 'newVal' can be a Map or map[string]interface{} value with a single 'key' that is the key to be modified
- // or a string value "key:value[:type]" where type is "bool" or "num" to cast the value.
- // 'path' is dot-notation list of keys to traverse; last key in path can be newVal key
- // NOTE: 'path' spec does not currently support indexed array references.
- // 'subkeys' are "key:value[:type]" entries that must match for path node
- // - For attributes prefix the label with the attribute prefix character, by default a
- // hyphen, '-', e.g., "-seq:3". (See SetAttrPrefix function.)
- // - The subkey can be wildcarded - "key:*" - to require that it's there with some value.
- // - If a subkey is preceeded with the '!' character, the key:value[:type] entry is treated as an
- // exclusion critera - e.g., "!author:William T. Gaddis".
- //
- // NOTES:
- // 1. Simple elements with attributes need a path terminated as ".#text" to modify the actual value.
- // 2. Values in Maps created using NewMapXmlSeq are map[string]interface{} values with a "#text" key.
- // 3. If values in 'newVal' or 'subkeys' args contain ":", use SetFieldSeparator to an unused symbol,
- // perhaps "|".
- func (mv Map) UpdateValuesForPath(newVal interface{}, path string, subkeys ...string) (int, error) {
- m := map[string]interface{}(mv)
- // extract the subkeys
- var subKeyMap map[string]interface{}
- if len(subkeys) > 0 {
- var err error
- subKeyMap, err = getSubKeyMap(subkeys...)
- if err != nil {
- return 0, err
- }
- }
- // extract key and value from newVal
- var key string
- var val interface{}
- switch newVal.(type) {
- case map[string]interface{}, Map:
- switch newVal.(type) { // "fallthrough is not permitted in type switch" (Spec)
- case Map:
- newVal = newVal.(Map).Old()
- }
- if len(newVal.(map[string]interface{})) != 1 {
- return 0, fmt.Errorf("newVal map can only have len == 1 - %+v", newVal)
- }
- for key, val = range newVal.(map[string]interface{}) {
- }
- case string: // split it as a key:value pair
- ss := strings.Split(newVal.(string), fieldSep)
- n := len(ss)
- if n < 2 || n > 3 {
- return 0, fmt.Errorf("unknown newVal spec - %+v", newVal)
- }
- key = ss[0]
- if n == 2 {
- val = interface{}(ss[1])
- } else if n == 3 {
- switch ss[2] {
- case "bool", "boolean":
- nv, err := strconv.ParseBool(ss[1])
- if err != nil {
- return 0, fmt.Errorf("can't convert newVal to bool - %+v", newVal)
- }
- val = interface{}(nv)
- case "num", "numeric", "float", "int":
- nv, err := strconv.ParseFloat(ss[1], 64)
- if err != nil {
- return 0, fmt.Errorf("can't convert newVal to float64 - %+v", newVal)
- }
- val = interface{}(nv)
- default:
- return 0, fmt.Errorf("unknown type for newVal value - %+v", newVal)
- }
- }
- default:
- return 0, fmt.Errorf("invalid newVal type - %+v", newVal)
- }
- // parse path
- keys := strings.Split(path, ".")
- var count int
- updateValuesForKeyPath(key, val, m, keys, subKeyMap, &count)
- return count, nil
- }
- // navigate the path
- func updateValuesForKeyPath(key string, value interface{}, m interface{}, keys []string, subkeys map[string]interface{}, cnt *int) {
- // ----- at end node: looking at possible node to get 'key' ----
- if len(keys) == 1 {
- updateValue(key, value, m, keys[0], subkeys, cnt)
- return
- }
- // ----- here we are navigating the path thru the penultimate node --------
- // key of interest is keys[0] - the next in the path
- switch keys[0] {
- case "*": // wildcard - scan all values
- switch m.(type) {
- case map[string]interface{}:
- for _, v := range m.(map[string]interface{}) {
- updateValuesForKeyPath(key, value, v, keys[1:], subkeys, cnt)
- }
- case []interface{}:
- for _, v := range m.([]interface{}) {
- switch v.(type) {
- // flatten out a list of maps - keys are processed
- case map[string]interface{}:
- for _, vv := range v.(map[string]interface{}) {
- updateValuesForKeyPath(key, value, vv, keys[1:], subkeys, cnt)
- }
- default:
- updateValuesForKeyPath(key, value, v, keys[1:], subkeys, cnt)
- }
- }
- }
- default: // key - must be map[string]interface{}
- switch m.(type) {
- case map[string]interface{}:
- if v, ok := m.(map[string]interface{})[keys[0]]; ok {
- updateValuesForKeyPath(key, value, v, keys[1:], subkeys, cnt)
- }
- case []interface{}: // may be buried in list
- for _, v := range m.([]interface{}) {
- switch v.(type) {
- case map[string]interface{}:
- if vv, ok := v.(map[string]interface{})[keys[0]]; ok {
- updateValuesForKeyPath(key, value, vv, keys[1:], subkeys, cnt)
- }
- }
- }
- }
- }
- }
- // change value if key and subkeys are present
- func updateValue(key string, value interface{}, m interface{}, keys0 string, subkeys map[string]interface{}, cnt *int) {
- // there are two possible options for the value of 'keys0': map[string]interface, []interface{}
- // and 'key' is a key in the map or is a key in a map in a list.
- switch m.(type) {
- case map[string]interface{}: // gotta have the last key
- if keys0 == "*" {
- for k := range m.(map[string]interface{}) {
- updateValue(key, value, m, k, subkeys, cnt)
- }
- return
- }
- endVal, _ := m.(map[string]interface{})[keys0]
- // if newV key is the end of path, replace the value for path-end
- // may be []interface{} - means replace just an entry w/ subkeys
- // otherwise replace the keys0 value if subkeys are there
- // NOTE: this will replace the subkeys, also
- if key == keys0 {
- switch endVal.(type) {
- case map[string]interface{}:
- if hasSubKeys(m, subkeys) {
- (m.(map[string]interface{}))[keys0] = value
- (*cnt)++
- }
- case []interface{}:
- // without subkeys can't select list member to modify
- // so key:value spec is it ...
- if hasSubKeys(m, subkeys) {
- (m.(map[string]interface{}))[keys0] = value
- (*cnt)++
- break
- }
- nv := make([]interface{}, 0)
- var valmodified bool
- for _, v := range endVal.([]interface{}) {
- // check entry subkeys
- if hasSubKeys(v, subkeys) {
- // replace v with value
- nv = append(nv, value)
- valmodified = true
- (*cnt)++
- continue
- }
- nv = append(nv, v)
- }
- if valmodified {
- (m.(map[string]interface{}))[keys0] = interface{}(nv)
- }
- default: // anything else is a strict replacement
- if hasSubKeys(m, subkeys) {
- (m.(map[string]interface{}))[keys0] = value
- (*cnt)++
- }
- }
- return
- }
- // so value is for an element of endVal
- // if endVal is a map then 'key' must be there w/ subkeys
- // if endVal is a list then 'key' must be in a list member w/ subkeys
- switch endVal.(type) {
- case map[string]interface{}:
- if !hasSubKeys(endVal, subkeys) {
- return
- }
- if _, ok := (endVal.(map[string]interface{}))[key]; ok {
- (endVal.(map[string]interface{}))[key] = value
- (*cnt)++
- }
- case []interface{}: // keys0 points to a list, check subkeys
- for _, v := range endVal.([]interface{}) {
- // got to be a map so we can replace value for 'key'
- vv, vok := v.(map[string]interface{})
- if !vok {
- continue
- }
- if _, ok := vv[key]; !ok {
- continue
- }
- if !hasSubKeys(vv, subkeys) {
- continue
- }
- vv[key] = value
- (*cnt)++
- }
- }
- case []interface{}: // key may be in a list member
- // don't need to handle keys0 == "*"; we're looking at everything, anyway.
- for _, v := range m.([]interface{}) {
- // only map values - we're looking for 'key'
- mm, ok := v.(map[string]interface{})
- if !ok {
- continue
- }
- if _, ok := mm[key]; !ok {
- continue
- }
- if !hasSubKeys(mm, subkeys) {
- continue
- }
- mm[key] = value
- (*cnt)++
- }
- }
- // return
- }
|