unmarshal.go (3747B)
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 transform 15 16 import ( 17 "fmt" 18 "io/ioutil" 19 "strings" 20 21 "github.com/gohugoio/hugo/resources/resource" 22 23 "github.com/gohugoio/hugo/common/types" 24 25 "github.com/mitchellh/mapstructure" 26 27 "errors" 28 29 "github.com/gohugoio/hugo/helpers" 30 "github.com/gohugoio/hugo/parser/metadecoders" 31 32 "github.com/spf13/cast" 33 ) 34 35 // Unmarshal unmarshals the data given, which can be either a string, json.RawMessage 36 // or a Resource. Supported formats are JSON, TOML, YAML, and CSV. 37 // You can optionally provide an options map as the first argument. 38 func (ns *Namespace) Unmarshal(args ...any) (any, error) { 39 if len(args) < 1 || len(args) > 2 { 40 return nil, errors.New("unmarshal takes 1 or 2 arguments") 41 } 42 43 var data any 44 decoder := metadecoders.Default 45 46 if len(args) == 1 { 47 data = args[0] 48 } else { 49 m, ok := args[0].(map[string]any) 50 if !ok { 51 return nil, errors.New("first argument must be a map") 52 } 53 54 var err error 55 56 data = args[1] 57 decoder, err = decodeDecoder(m) 58 if err != nil { 59 return nil, fmt.Errorf("failed to decode options: %w", err) 60 } 61 } 62 63 if r, ok := data.(resource.UnmarshableResource); ok { 64 key := r.Key() 65 66 if key == "" { 67 return nil, errors.New("no Key set in Resource") 68 } 69 70 if decoder != metadecoders.Default { 71 key += decoder.OptionsKey() 72 } 73 74 return ns.cache.GetOrCreate(key, func() (any, error) { 75 f := metadecoders.FormatFromMediaType(r.MediaType()) 76 if f == "" { 77 return nil, fmt.Errorf("MIME %q not supported", r.MediaType()) 78 } 79 80 reader, err := r.ReadSeekCloser() 81 if err != nil { 82 return nil, err 83 } 84 defer reader.Close() 85 86 b, err := ioutil.ReadAll(reader) 87 if err != nil { 88 return nil, err 89 } 90 91 return decoder.Unmarshal(b, f) 92 }) 93 } 94 95 dataStr, err := types.ToStringE(data) 96 if err != nil { 97 return nil, fmt.Errorf("type %T not supported", data) 98 } 99 100 if dataStr == "" { 101 return nil, errors.New("no data to transform") 102 } 103 104 key := helpers.MD5String(dataStr) 105 106 return ns.cache.GetOrCreate(key, func() (any, error) { 107 f := decoder.FormatFromContentString(dataStr) 108 if f == "" { 109 return nil, errors.New("unknown format") 110 } 111 112 return decoder.Unmarshal([]byte(dataStr), f) 113 }) 114 } 115 116 func decodeDecoder(m map[string]any) (metadecoders.Decoder, error) { 117 opts := metadecoders.Default 118 119 if m == nil { 120 return opts, nil 121 } 122 123 // mapstructure does not support string to rune conversion, so do that manually. 124 // See https://github.com/mitchellh/mapstructure/issues/151 125 for k, v := range m { 126 if strings.EqualFold(k, "Delimiter") { 127 r, err := stringToRune(v) 128 if err != nil { 129 return opts, err 130 } 131 opts.Delimiter = r 132 delete(m, k) 133 134 } else if strings.EqualFold(k, "Comment") { 135 r, err := stringToRune(v) 136 if err != nil { 137 return opts, err 138 } 139 opts.Comment = r 140 delete(m, k) 141 } 142 } 143 144 err := mapstructure.WeakDecode(m, &opts) 145 146 return opts, err 147 } 148 149 func stringToRune(v any) (rune, error) { 150 s, err := cast.ToStringE(v) 151 if err != nil { 152 return 0, err 153 } 154 155 if len(s) == 0 { 156 return 0, nil 157 } 158 159 var r rune 160 161 for i, rr := range s { 162 if i == 0 { 163 r = rr 164 } else { 165 return 0, fmt.Errorf("invalid character: %q", v) 166 } 167 } 168 169 return r, nil 170 }