From f8d555cca59a48df9cde2e7323ff2d500e0590a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 3 Apr 2017 22:39:37 +0200 Subject: [PATCH] media: Add DecodeTypes And clean up the media package. --- media/mediaType.go | 132 +++++++++++++++++++++++++++++++++--- media/mediaType_test.go | 78 +++++++++++++++++++++ output/outputFormat.go | 4 +- output/outputFormat_test.go | 2 +- 4 files changed, 202 insertions(+), 14 deletions(-) diff --git a/media/mediaType.go b/media/mediaType.go index b56904cd9..bc54986be 100644 --- a/media/mediaType.go +++ b/media/mediaType.go @@ -15,20 +15,12 @@ package media import ( "fmt" + "sort" "strings" + + "github.com/mitchellh/mapstructure" ) -type Types []Type - -func (t Types) GetByType(tp string) (Type, bool) { - for _, tt := range t { - if strings.EqualFold(tt.Type(), tp) { - return tt, true - } - } - return Type{}, false -} - // A media type (also known as MIME type and content type) is a two-part identifier for // file formats and format contents transmitted on the Internet. // For Hugo's use case, we use the top-level type name / subtype name + suffix. @@ -41,6 +33,29 @@ type Type struct { Suffix string // i.e html } +// FromTypeString creates a new Type given a type sring on the form MainType/SubType and +// an optional suffix, e.g. "text/html" or "text/html+html". +func FromString(t string) (Type, error) { + t = strings.ToLower(t) + parts := strings.Split(t, "/") + if len(parts) != 2 { + return Type{}, fmt.Errorf("cannot parse %q as a media type", t) + } + mainType := parts[0] + subParts := strings.Split(parts[1], "+") + + subType := subParts[0] + var suffix string + + if len(subParts) == 1 { + suffix = subType + } else { + suffix = subParts[1] + } + + return Type{MainType: mainType, SubType: subType, Suffix: suffix}, nil +} + // Type returns a string representing the main- and sub-type of a media type, i.e. "text/css". // Hugo will register a set of default media types. // These can be overridden by the user in the configuration, @@ -68,4 +83,99 @@ var ( TextType = Type{"text", "plain", "txt"} ) +var DefaultTypes = Types{ + CalendarType, + CSSType, + CSVType, + HTMLType, + JavascriptType, + JSONType, + RSSType, + XMLType, + TextType, +} + +func init() { + sort.Sort(DefaultTypes) +} + +type Types []Type + +func (t Types) Len() int { return len(t) } +func (t Types) Swap(i, j int) { t[i], t[j] = t[j], t[i] } +func (t Types) Less(i, j int) bool { return t[i].Type() < t[j].Type() } + +func (t Types) GetByType(tp string) (Type, bool) { + for _, tt := range t { + if strings.EqualFold(tt.Type(), tp) { + return tt, true + } + } + return Type{}, false +} + +// GetBySuffix gets a media type given as suffix, e.g. "html". +// It will return false if no format could be found, or if the suffix given +// is ambiguous. +// The lookup is case insensitive. +func (t Types) GetBySuffix(suffix string) (tp Type, found bool) { + for _, tt := range t { + if strings.EqualFold(suffix, tt.Suffix) { + if found { + // ambiguous + found = false + return + } + tp = tt + found = true + } + } + return +} + +// DecodeTypes takes a list of media type configurations and merges those, +// in ther order given, with the Hugo defaults as the last resort. +func DecodeTypes(maps ...map[string]interface{}) (Types, error) { + m := make(Types, len(DefaultTypes)) + copy(m, DefaultTypes) + + for _, mm := range maps { + for k, v := range mm { + // It may be tempting to put the full media type in the key, e.g. + // "text/css+css", but that will break the logic below. + if strings.Contains(k, "+") { + return Types{}, fmt.Errorf("media type keys cannot contain any '+' chars. Valid example is %q", "text/css") + } + + found := false + for i, vv := range m { + // Match by type, i.e. "text/css" + if strings.EqualFold(k, vv.Type()) { + // Merge it with the existing + if err := mapstructure.WeakDecode(v, &m[i]); err != nil { + return m, err + } + found = true + } + } + if !found { + mediaType, err := FromString(k) + if err != nil { + return m, err + } + + if err := mapstructure.WeakDecode(v, &mediaType); err != nil { + return m, err + } + + m = append(m, mediaType) + } + } + } + + sort.Sort(m) + + return m, nil +} + // TODO(bep) output mime.AddExtensionType diff --git a/media/mediaType_test.go b/media/mediaType_test.go index c97ac782a..8d83c19f8 100644 --- a/media/mediaType_test.go +++ b/media/mediaType_test.go @@ -58,3 +58,81 @@ func TestGetByType(t *testing.T) { _, found = types.GetByType("text/nono") require.False(t, found) } + +func TestFromTypeString(t *testing.T) { + f, err := FromString("text/html") + require.NoError(t, err) + require.Equal(t, HTMLType, f) + + f, err = FromString("application/custom") + require.NoError(t, err) + require.Equal(t, Type{MainType: "application", SubType: "custom", Suffix: "custom"}, f) + + f, err = FromString("application/custom+pdf") + require.NoError(t, err) + require.Equal(t, Type{MainType: "application", SubType: "custom", Suffix: "pdf"}, f) + + f, err = FromString("noslash") + require.Error(t, err) + +} + +func TestDecodeTypes(t *testing.T) { + + var tests = []struct { + name string + maps []map[string]interface{} + shouldError bool + assert func(t *testing.T, name string, tt Types) + }{ + { + "Redefine JSON", + []map[string]interface{}{ + map[string]interface{}{ + "application/json": map[string]interface{}{ + "suffix": "jsn"}}}, + false, + func(t *testing.T, name string, tt Types) { + require.Len(t, tt, len(DefaultTypes)) + json, found := tt.GetBySuffix("jsn") + require.True(t, found) + require.Equal(t, "application/json+jsn", json.String(), name) + }}, + { + "Add custom media type", + []map[string]interface{}{ + map[string]interface{}{ + "text/hugo": map[string]interface{}{ + "suffix": "hgo"}}}, + false, + func(t *testing.T, name string, tt Types) { + require.Len(t, tt, len(DefaultTypes)+1) + // Make sure we have not broken the default config. + _, found := tt.GetBySuffix("json") + require.True(t, found) + + hugo, found := tt.GetBySuffix("hgo") + require.True(t, found) + require.Equal(t, "text/hugo+hgo", hugo.String(), name) + }}, + { + "Add media type invalid key", + []map[string]interface{}{ + map[string]interface{}{ + "text/hugo+hgo": map[string]interface{}{}}}, + true, + func(t *testing.T, name string, tt Types) { + + }}, + } + + for _, test := range tests { + result, err := DecodeTypes(test.maps...) + if test.shouldError { + require.Error(t, err, test.name) + } else { + require.NoError(t, err, test.name) + test.assert(t, test.name, result) + } + } +} diff --git a/output/outputFormat.go b/output/outputFormat.go index 99420f720..06a82fdb7 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -216,9 +216,9 @@ func (formats Formats) FromFilename(filename string) (f Format, found bool) { return } -// DecodeOutputFormats takes a list of output format configurations and merges those, +// DecodeFormats takes a list of output format configurations and merges those, // in ther order given, with the Hugo defaults as the last resort. -func DecodeOutputFormats(mediaTypes media.Types, maps ...map[string]interface{}) (Formats, error) { +func DecodeFormats(mediaTypes media.Types, maps ...map[string]interface{}) (Formats, error) { f := make(Formats, len(DefaultFormats)) copy(f, DefaultFormats) diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go index 48937a8f1..78b2a6096 100644 --- a/output/outputFormat_test.go +++ b/output/outputFormat_test.go @@ -174,7 +174,7 @@ func TestDecodeFormats(t *testing.T) { } for _, test := range tests { - result, err := DecodeOutputFormats(mediaTypes, test.maps...) + result, err := DecodeFormats(mediaTypes, test.maps...) if test.shouldError { require.Error(t, err, test.name) } else {