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 }