// Copyright 2019 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package transform import ( "errors" "fmt" "io" "strings" "github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/common/hashing" "github.com/gohugoio/hugo/common/types" "github.com/mitchellh/mapstructure" "github.com/gohugoio/hugo/parser/metadecoders" "github.com/spf13/cast" ) // Unmarshal unmarshals the data given, which can be either a string, json.RawMessage // or a Resource. Supported formats are JSON, TOML, YAML, and CSV. // You can optionally provide an options map as the first argument. func (ns *Namespace) Unmarshal(args ...any) (any, error) { if len(args) < 1 || len(args) > 2 { return nil, errors.New("unmarshal takes 1 or 2 arguments") } var data any decoder := metadecoders.Default if len(args) == 1 { data = args[0] } else { m, ok := args[0].(map[string]any) if !ok { return nil, errors.New("first argument must be a map") } var err error data = args[1] decoder, err = decodeDecoder(m) if err != nil { return nil, fmt.Errorf("failed to decode options: %w", err) } } if r, ok := data.(resource.UnmarshableResource); ok { key := r.Key() if key == "" { return nil, errors.New("no Key set in Resource") } if decoder != metadecoders.Default { key += decoder.OptionsKey() } v, err := ns.cacheUnmarshal.GetOrCreate(key, func(string) (*resources.StaleValue[any], error) { f := metadecoders.FormatFromStrings(r.MediaType().Suffixes()...) if f == "" { return nil, fmt.Errorf("MIME %q not supported", r.MediaType()) } reader, err := r.ReadSeekCloser() if err != nil { return nil, err } defer reader.Close() b, err := io.ReadAll(reader) if err != nil { return nil, err } v, err := decoder.Unmarshal(b, f) if err != nil { return nil, err } return &resources.StaleValue[any]{ Value: v, StaleVersionFunc: func() uint32 { return resource.StaleVersion(r) }, }, nil }) if err != nil { return nil, err } return v.Value, nil } dataStr, err := types.ToStringE(data) if err != nil { return nil, fmt.Errorf("type %T not supported", data) } if strings.TrimSpace(dataStr) == "" { return nil, nil } key := hashing.MD5FromStringHexEncoded(dataStr) v, err := ns.cacheUnmarshal.GetOrCreate(key, func(string) (*resources.StaleValue[any], error) { f := decoder.FormatFromContentString(dataStr) if f == "" { return nil, errors.New("unknown format") } v, err := decoder.Unmarshal([]byte(dataStr), f) if err != nil { return nil, err } return &resources.StaleValue[any]{ Value: v, StaleVersionFunc: func() uint32 { return 0 }, }, nil }) if err != nil { return nil, err } return v.Value, nil } func decodeDecoder(m map[string]any) (metadecoders.Decoder, error) { opts := metadecoders.Default if m == nil { return opts, nil } // mapstructure does not support string to rune conversion, so do that manually. // See https://github.com/mitchellh/mapstructure/issues/151 for k, v := range m { if strings.EqualFold(k, "Delimiter") { r, err := stringToRune(v) if err != nil { return opts, err } opts.Delimiter = r delete(m, k) } else if strings.EqualFold(k, "Comment") { r, err := stringToRune(v) if err != nil { return opts, err } opts.Comment = r delete(m, k) } } err := mapstructure.WeakDecode(m, &opts) return opts, err } func stringToRune(v any) (rune, error) { s, err := cast.ToStringE(v) if err != nil { return 0, err } if len(s) == 0 { return 0, nil } var r rune for i, rr := range s { if i == 0 { r = rr } else { return 0, fmt.Errorf("invalid character: %q", v) } } return r, nil }