github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/common/maps/params.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package maps
    15  
    16  import (
    17  	"fmt"
    18  	"strings"
    19  
    20  	"github.com/spf13/cast"
    21  )
    22  
    23  // Params is a map where all keys are lower case.
    24  type Params map[string]any
    25  
    26  // Get does a lower case and nested search in this map.
    27  // It will return nil if none found.
    28  func (p Params) Get(indices ...string) any {
    29  	v, _, _ := getNested(p, indices)
    30  	return v
    31  }
    32  
    33  // Set overwrites values in p with values in pp for common or new keys.
    34  // This is done recursively.
    35  func (p Params) Set(pp Params) {
    36  	for k, v := range pp {
    37  		vv, found := p[k]
    38  		if !found {
    39  			p[k] = v
    40  		} else {
    41  			switch vvv := vv.(type) {
    42  			case Params:
    43  				if pv, ok := v.(Params); ok {
    44  					vvv.Set(pv)
    45  				} else {
    46  					p[k] = v
    47  				}
    48  			default:
    49  				p[k] = v
    50  			}
    51  		}
    52  	}
    53  }
    54  
    55  // IsZero returns true if p is considered empty.
    56  func (p Params) IsZero() bool {
    57  	if p == nil || len(p) == 0 {
    58  		return true
    59  	}
    60  
    61  	if len(p) > 1 {
    62  		return false
    63  	}
    64  
    65  	for k, _ := range p {
    66  		return k == mergeStrategyKey
    67  	}
    68  
    69  	return false
    70  
    71  }
    72  
    73  // Merge transfers values from pp to p for new keys.
    74  // This is done recursively.
    75  func (p Params) Merge(pp Params) {
    76  	p.merge("", pp)
    77  }
    78  
    79  // MergeRoot transfers values from pp to p for new keys where p is the
    80  // root of the tree.
    81  // This is done recursively.
    82  func (p Params) MergeRoot(pp Params) {
    83  	ms, _ := p.GetMergeStrategy()
    84  	p.merge(ms, pp)
    85  }
    86  
    87  func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
    88  	ns, found := p.GetMergeStrategy()
    89  
    90  	var ms = ns
    91  	if !found && ps != "" {
    92  		ms = ps
    93  	}
    94  
    95  	noUpdate := ms == ParamsMergeStrategyNone
    96  	noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow)
    97  
    98  	for k, v := range pp {
    99  
   100  		if k == mergeStrategyKey {
   101  			continue
   102  		}
   103  		vv, found := p[k]
   104  
   105  		if found {
   106  			// Key matches, if both sides are Params, we try to merge.
   107  			if vvv, ok := vv.(Params); ok {
   108  				if pv, ok := v.(Params); ok {
   109  					vvv.merge(ms, pv)
   110  				}
   111  			}
   112  		} else if !noUpdate {
   113  			p[k] = v
   114  		}
   115  
   116  	}
   117  }
   118  
   119  func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
   120  	if v, found := p[mergeStrategyKey]; found {
   121  		if s, ok := v.(ParamsMergeStrategy); ok {
   122  			return s, true
   123  		}
   124  	}
   125  	return ParamsMergeStrategyShallow, false
   126  }
   127  
   128  func (p Params) DeleteMergeStrategy() bool {
   129  	if _, found := p[mergeStrategyKey]; found {
   130  		delete(p, mergeStrategyKey)
   131  		return true
   132  	}
   133  	return false
   134  }
   135  
   136  func (p Params) SetDefaultMergeStrategy(s ParamsMergeStrategy) {
   137  	switch s {
   138  	case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
   139  	default:
   140  		panic(fmt.Sprintf("invalid merge strategy %q", s))
   141  	}
   142  	p[mergeStrategyKey] = s
   143  }
   144  
   145  func getNested(m map[string]any, indices []string) (any, string, map[string]any) {
   146  	if len(indices) == 0 {
   147  		return nil, "", nil
   148  	}
   149  
   150  	first := indices[0]
   151  	v, found := m[strings.ToLower(cast.ToString(first))]
   152  	if !found {
   153  		if len(indices) == 1 {
   154  			return nil, first, m
   155  		}
   156  		return nil, "", nil
   157  
   158  	}
   159  
   160  	if len(indices) == 1 {
   161  		return v, first, m
   162  	}
   163  
   164  	switch m2 := v.(type) {
   165  	case Params:
   166  		return getNested(m2, indices[1:])
   167  	case map[string]any:
   168  		return getNested(m2, indices[1:])
   169  	default:
   170  		return nil, "", nil
   171  	}
   172  }
   173  
   174  // GetNestedParam gets the first match of the keyStr in the candidates given.
   175  // It will first try the exact match and then try to find it as a nested map value,
   176  // using the given separator, e.g. "mymap.name".
   177  // It assumes that all the maps given have lower cased keys.
   178  func GetNestedParam(keyStr, separator string, candidates ...Params) (any, error) {
   179  	keyStr = strings.ToLower(keyStr)
   180  
   181  	// Try exact match first
   182  	for _, m := range candidates {
   183  		if v, ok := m[keyStr]; ok {
   184  			return v, nil
   185  		}
   186  	}
   187  
   188  	keySegments := strings.Split(keyStr, separator)
   189  	for _, m := range candidates {
   190  		if v := m.Get(keySegments...); v != nil {
   191  			return v, nil
   192  		}
   193  	}
   194  
   195  	return nil, nil
   196  }
   197  
   198  func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) any) (any, string, map[string]any, error) {
   199  	keySegments := strings.Split(keyStr, separator)
   200  	if len(keySegments) == 0 {
   201  		return nil, "", nil, nil
   202  	}
   203  
   204  	first := lookupFn(keySegments[0])
   205  	if first == nil {
   206  		return nil, "", nil, nil
   207  	}
   208  
   209  	if len(keySegments) == 1 {
   210  		return first, keySegments[0], nil, nil
   211  	}
   212  
   213  	switch m := first.(type) {
   214  	case map[string]any:
   215  		v, key, owner := getNested(m, keySegments[1:])
   216  		return v, key, owner, nil
   217  	case Params:
   218  		v, key, owner := getNested(m, keySegments[1:])
   219  		return v, key, owner, nil
   220  	}
   221  
   222  	return nil, "", nil, nil
   223  }
   224  
   225  // ParamsMergeStrategy tells what strategy to use in Params.Merge.
   226  type ParamsMergeStrategy string
   227  
   228  const (
   229  	// Do not merge.
   230  	ParamsMergeStrategyNone ParamsMergeStrategy = "none"
   231  	// Only add new keys.
   232  	ParamsMergeStrategyShallow ParamsMergeStrategy = "shallow"
   233  	// Add new keys, merge existing.
   234  	ParamsMergeStrategyDeep ParamsMergeStrategy = "deep"
   235  
   236  	mergeStrategyKey = "_merge"
   237  )
   238  
   239  func toMergeStrategy(v any) ParamsMergeStrategy {
   240  	s := ParamsMergeStrategy(cast.ToString(v))
   241  	switch s {
   242  	case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
   243  		return s
   244  	default:
   245  		return ParamsMergeStrategyDeep
   246  	}
   247  }
   248  
   249  // PrepareParams
   250  // * makes all the keys in the given map lower cased and will do so
   251  // * This will modify the map given.
   252  // * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string  will be converted to Params.
   253  // * Any _merge value will be converted to proper type and value.
   254  func PrepareParams(m Params) {
   255  	for k, v := range m {
   256  		var retyped bool
   257  		lKey := strings.ToLower(k)
   258  		if lKey == mergeStrategyKey {
   259  			v = toMergeStrategy(v)
   260  			retyped = true
   261  		} else {
   262  			switch vv := v.(type) {
   263  			case map[any]any:
   264  				var p Params = cast.ToStringMap(v)
   265  				v = p
   266  				PrepareParams(p)
   267  				retyped = true
   268  			case map[string]any:
   269  				var p Params = v.(map[string]any)
   270  				v = p
   271  				PrepareParams(p)
   272  				retyped = true
   273  			case map[string]string:
   274  				p := make(Params)
   275  				for k, v := range vv {
   276  					p[k] = v
   277  				}
   278  				v = p
   279  				PrepareParams(p)
   280  				retyped = true
   281  			}
   282  		}
   283  
   284  		if retyped || k != lKey {
   285  			delete(m, k)
   286  			m[lKey] = v
   287  		}
   288  	}
   289  }