diff --git a/parser/metadecoders/decoder.go b/parser/metadecoders/decoder.go index 419fbf4d2..83b4b04cd 100644 --- a/parser/metadecoders/decoder.go +++ b/parser/metadecoders/decoder.go @@ -36,6 +36,10 @@ import ( // Decoder provides some configuration options for the decoders. type Decoder struct { + // Format specifies a specific format to decode from. If empty or + // unspecified, it's inferred from the contents or the filename. + Format string + // Delimiter is the field delimiter. Used in the CSV decoder. Default is // ','. Delimiter rune @@ -57,6 +61,7 @@ type Decoder struct { // OptionsKey is used in cache keys. func (d Decoder) OptionsKey() string { var sb strings.Builder + sb.WriteString(d.Format) sb.WriteRune(d.Delimiter) sb.WriteRune(d.Comment) sb.WriteString(strconv.FormatBool(d.LazyQuotes)) diff --git a/tpl/transform/unmarshal.go b/tpl/transform/unmarshal.go index a3a9c040a..d0edda453 100644 --- a/tpl/transform/unmarshal.go +++ b/tpl/transform/unmarshal.go @@ -72,9 +72,17 @@ func (ns *Namespace) Unmarshal(args ...any) (any, error) { } 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()) + var f metadecoders.Format + if decoder.Format != "" { + f = metadecoders.FormatFromString(decoder.Format) + if f == "" { + return nil, fmt.Errorf("format %q not supported", decoder.Format) + } + } else { + f = metadecoders.FormatFromStrings(r.MediaType().Suffixes()...) + if f == "" { + return nil, fmt.Errorf("MIME %q not supported", r.MediaType()) + } } reader, err := r.ReadSeekCloser() @@ -119,10 +127,22 @@ func (ns *Namespace) Unmarshal(args ...any) (any, error) { key := hashing.MD5FromStringHexEncoded(dataStr) + if decoder != metadecoders.Default { + key += decoder.OptionsKey() + } + v, err := ns.cacheUnmarshal.GetOrCreate(key, func(string) (*resources.StaleValue[any], error) { - f := decoder.FormatFromContentString(dataStr) - if f == "" { - return nil, errors.New("unknown format") + var f metadecoders.Format + if decoder.Format != "" { + f = metadecoders.FormatFromString(decoder.Format) + if f == "" { + return nil, fmt.Errorf("format %q not supported", decoder.Format) + } + } else { + f = decoder.FormatFromContentString(dataStr) + if f == "" { + return nil, errors.New("unknown format") + } } v, err := decoder.Unmarshal([]byte(dataStr), f) diff --git a/tpl/transform/unmarshal_test.go b/tpl/transform/unmarshal_test.go index 9b34e1daa..934b29ee1 100644 --- a/tpl/transform/unmarshal_test.go +++ b/tpl/transform/unmarshal_test.go @@ -128,6 +128,9 @@ func TestUnmarshal(t *testing.T) { {testContentResource{key: "r1", content: `a;b;c`, mime: media.Builtin.CSVType}, map[string]any{"delimiter": ";"}, func(r [][]string) { b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r) }}, + {testContentResource{key: "r1", content: `{p: [a, b]}`, mime: media.Builtin.JSONType}, map[string]any{"format": "yaml"}, func(m map[string]any) { + b.Assert(m, qt.DeepEquals, map[string]any{"p": []any{"a", "b"}}) + }}, {"a,b,c", nil, func(r [][]string) { b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r) }}, @@ -141,6 +144,9 @@ a;b;c`, mime: media.Builtin.CSVType}, map[string]any{"DElimiter": ";", "Comment" }}, {``, nil, nil}, {` `, nil, nil}, + {`{p: [a, b]}`, map[string]any{"format": "yaml"}, func(m map[string]any) { + b.Assert(m, qt.DeepEquals, map[string]any{"p": []any{"a", "b"}}) + }}, // errors {"thisisnotavaliddataformat", nil, false}, {testContentResource{key: "r1", content: `invalid&toml"`, mime: media.Builtin.TOMLType}, nil, false}, @@ -148,6 +154,8 @@ a;b;c`, mime: media.Builtin.CSVType}, map[string]any{"DElimiter": ";", "Comment" {"thisisnotavaliddataformat", nil, false}, {`{ notjson }`, nil, false}, {tstNoStringer{}, nil, false}, + {"notjson", map[string]any{"format": "json"}, false}, + {"anything", map[string]any{"format": "invalidformat"}, false}, } { ns.Reset()