params.go (6555B)
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 }