merge.go (3311B)
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 collections
15
16 import (
17 "fmt"
18 "reflect"
19 "strings"
20
21 "github.com/gohugoio/hugo/common/hreflect"
22 "github.com/gohugoio/hugo/common/maps"
23
24 "errors"
25 )
26
27 // Merge creates a copy of the final parameter and merges the preceding
28 // parameters into it in reverse order.
29 // Currently only maps are supported. Key handling is case insensitive.
30 func (ns *Namespace) Merge(params ...any) (any, error) {
31 if len(params) < 2 {
32 return nil, errors.New("merge requires at least two parameters")
33 }
34
35 var err error
36 result := params[len(params)-1]
37
38 for i := len(params) - 2; i >= 0; i-- {
39 result, err = ns.merge(params[i], result)
40 if err != nil {
41 return nil, err
42 }
43 }
44
45 return result, nil
46 }
47
48 // merge creates a copy of dst and merges src into it.
49 func (ns *Namespace) merge(src, dst any) (any, error) {
50 vdst, vsrc := reflect.ValueOf(dst), reflect.ValueOf(src)
51
52 if vdst.Kind() != reflect.Map {
53 return nil, fmt.Errorf("destination must be a map, got %T", dst)
54 }
55
56 if !hreflect.IsTruthfulValue(vsrc) {
57 return dst, nil
58 }
59
60 if vsrc.Kind() != reflect.Map {
61 return nil, fmt.Errorf("source must be a map, got %T", src)
62 }
63
64 if vsrc.Type().Key() != vdst.Type().Key() {
65 return nil, fmt.Errorf("incompatible map types, got %T to %T", src, dst)
66 }
67
68 return mergeMap(vdst, vsrc).Interface(), nil
69 }
70
71 func caseInsensitiveLookup(m, k reflect.Value) (reflect.Value, bool) {
72 if m.Type().Key().Kind() != reflect.String || k.Kind() != reflect.String {
73 // Fall back to direct lookup.
74 v := m.MapIndex(k)
75 return v, hreflect.IsTruthfulValue(v)
76 }
77
78 for _, key := range m.MapKeys() {
79 if strings.EqualFold(k.String(), key.String()) {
80 return m.MapIndex(key), true
81 }
82 }
83
84 return reflect.Value{}, false
85 }
86
87 func mergeMap(dst, src reflect.Value) reflect.Value {
88 out := reflect.MakeMap(dst.Type())
89
90 // If the destination is Params, we must lower case all keys.
91 _, lowerCase := dst.Interface().(maps.Params)
92
93 // Copy the destination map.
94 for _, key := range dst.MapKeys() {
95 v := dst.MapIndex(key)
96 out.SetMapIndex(key, v)
97 }
98
99 // Add all keys in src not already in destination.
100 // Maps of the same type will be merged.
101 for _, key := range src.MapKeys() {
102 sv := src.MapIndex(key)
103 dv, found := caseInsensitiveLookup(dst, key)
104
105 if found {
106 // If both are the same map key type, merge.
107 dve := dv.Elem()
108 if dve.Kind() == reflect.Map {
109 sve := sv.Elem()
110 if sve.Kind() != reflect.Map {
111 continue
112 }
113
114 if dve.Type().Key() == sve.Type().Key() {
115 out.SetMapIndex(key, mergeMap(dve, sve))
116 }
117 }
118 } else {
119 if lowerCase && key.Kind() == reflect.String {
120 key = reflect.ValueOf(strings.ToLower(key.String()))
121 }
122 out.SetMapIndex(key, sv)
123 }
124 }
125
126 return out
127 }