Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added the resource/httplink package.
- Loading branch information
Showing
9 changed files
with
691 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// Copyright 2019 Google LLC | ||
// | ||
// 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 httplink defines a representation of Web Linkings. | ||
package httplink | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
"strings" | ||
) | ||
|
||
// Link represents a Web Linking [RFC 8288], aka. the Link HTTP header. | ||
type Link struct { | ||
URL *url.URL | ||
Params LinkParams | ||
} | ||
|
||
// NewLink creates and initializes a new Link with the provided URL u and | ||
// the provided rel parameter. | ||
func NewLink(u *url.URL, rel string) *Link { | ||
p := make(LinkParams, 1) | ||
p.Set(ParamRel, rel) | ||
return &Link{u, p} | ||
} | ||
|
||
// IsPreload reports whether the Link involves preloading of the resource. | ||
func (l *Link) IsPreload() bool { | ||
// TODO(yuizumi): Maybe include rel="prefetch" and similar. | ||
for _, s := range strings.Fields(l.Params.Get(ParamRel)) { | ||
if strings.EqualFold(s, RelPreload) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// String serializes the Link as it appears in the HTTP header. | ||
func (l *Link) String() string { | ||
var sb strings.Builder | ||
fmt.Fprintf(&sb, "<%s>", l.URL) | ||
l.Params.write(&sb) | ||
return sb.String() | ||
} | ||
|
||
// GoString implements the GoStringer interface. | ||
func (l *Link) GoString() string { | ||
return fmt.Sprintf("&httplink.Link{URL:&%#v, Params:%#v}", *l.URL, l.Params) | ||
} | ||
|
||
// Equal reports whether l and m have the same URL and parameters. The URLs | ||
// are compared by strings. | ||
func (l *Link) Equal(m *Link) bool { | ||
return l.URL.String() == m.URL.String() && l.Params.Equal(m.Params) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Copyright 2019 Google LLC | ||
// | ||
// 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 httplink_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/google/webpackager/internal/urlutil" | ||
"github.com/google/webpackager/resource/httplink" | ||
) | ||
|
||
func TestNewLink(t *testing.T) { | ||
u := urlutil.MustParse("https://example.com/style.css") | ||
rel := "preload" | ||
|
||
want := &httplink.Link{u, httplink.LinkParams{"rel": rel}} | ||
got := httplink.NewLink(u, rel) | ||
if diff := cmp.Diff(want, got); diff != "" { | ||
t.Errorf("NewLink(%q, %q) mismatch (-want +got):\n%s", u, rel, diff) | ||
} | ||
} | ||
|
||
func TestString(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
link *httplink.Link | ||
want string | ||
}{ | ||
{ | ||
name: "AbsoluteURL", | ||
link: &httplink.Link{ | ||
URL: urlutil.MustParse("https://example.com/style.css"), | ||
Params: httplink.LinkParams{ | ||
httplink.ParamRel: "preload", | ||
httplink.ParamAs: "style", | ||
}, | ||
}, | ||
want: `<https://example.com/style.css>;rel="preload";as="style"`, | ||
}, | ||
{ | ||
name: "RelativeURL", | ||
link: &httplink.Link{ | ||
URL: urlutil.MustParse("style.css"), | ||
Params: httplink.LinkParams{ | ||
httplink.ParamRel: "preload", | ||
httplink.ParamAs: "style", | ||
}, | ||
}, | ||
want: `<style.css>;rel="preload";as="style"`, | ||
}, | ||
{ | ||
name: "CrossOrigin_Anonymous", | ||
link: &httplink.Link{ | ||
URL: urlutil.MustParse("https://example.org/world.html"), | ||
Params: httplink.LinkParams{ | ||
httplink.ParamRel: "preload", | ||
httplink.ParamCrossOrigin: "anonymous", | ||
httplink.ParamAs: "document", | ||
}, | ||
}, | ||
want: `<https://example.org/world.html>;rel="preload";` + | ||
`as="document";crossorigin`, | ||
}, | ||
{ | ||
name: "CrossOrigin_UserCredentials", | ||
link: &httplink.Link{ | ||
URL: urlutil.MustParse("https://example.org/world.html"), | ||
Params: httplink.LinkParams{ | ||
httplink.ParamRel: "preload", | ||
httplink.ParamCrossOrigin: "user-credentials", | ||
httplink.ParamAs: "document", | ||
}, | ||
}, | ||
want: `<https://example.org/world.html>;rel="preload";` + | ||
`as="document";crossorigin="user-credentials"`, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
if diff := cmp.Diff(test.want, test.link.String()); diff != "" { | ||
t.Errorf("link.String() mismatch (-want +got):\n%s", diff) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
// Copyright 2019 Google LLC | ||
// | ||
// 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 httplink | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"reflect" | ||
"sort" | ||
"strings" | ||
) | ||
|
||
// A few common parameter names for use with LinkParams. | ||
const ( | ||
ParamRel = "rel" | ||
ParamAs = "as" | ||
ParamCrossOrigin = "crossorigin" | ||
ParamMedia = "media" | ||
ParamType = "type" | ||
) | ||
|
||
// Special parameter values recognized by LinkParams. | ||
const ( | ||
// Value(s) for the "rel" parameter. | ||
RelPreload = "preload" | ||
|
||
// Value(s) for the "crossorigin" parameter. | ||
CrossOriginAnonymous = "anonymous" | ||
) | ||
|
||
// LinkParams represents the parameters of a Web Linking. | ||
type LinkParams map[string]string | ||
|
||
// Get returns the value of the parameter specified by key. key gets lowered, | ||
// thus is case-insensitive. | ||
func (p LinkParams) Get(key string) string { | ||
return p[strings.ToLower(key)] | ||
} | ||
|
||
// Set changes the value of the parameter specified by key. key gets lowered, | ||
// thus is case-insensitive. Set also normalizes the provided value for some | ||
// parameters, e.g. removes extra spaces for the rel parameter. To get around | ||
// the normalization, access the map entry directly. | ||
func (p LinkParams) Set(key, val string) { | ||
key = strings.ToLower(key) | ||
p[key] = normalizeValue(key, val) | ||
} | ||
|
||
// Clone returns a deep copy of the LinkParams p. | ||
func (p LinkParams) Clone() LinkParams { | ||
q := make(LinkParams, len(p)) | ||
for k, v := range p { | ||
q[k] = v | ||
} | ||
return q | ||
} | ||
|
||
// Equal reports whether p and q contain the same set of key-value pairs. | ||
func (p LinkParams) Equal(q LinkParams) bool { | ||
return reflect.DeepEqual(p, q) | ||
} | ||
|
||
func (p LinkParams) write(w io.Writer) { | ||
keys := make([]string, 0, len(p)) | ||
for key := range p { | ||
keys = append(keys, key) | ||
} | ||
sort.Slice(keys, func(i, j int) bool { | ||
if keys[j] == ParamRel { | ||
return false | ||
} | ||
if keys[i] == ParamRel { | ||
return true | ||
} | ||
return keys[i] < keys[j] | ||
}) | ||
for _, key := range keys { | ||
if shouldElideValue(key, p[key]) { | ||
fmt.Fprintf(w, ";%s", key) | ||
} else { | ||
fmt.Fprintf(w, ";%s=%q", key, p[key]) | ||
} | ||
} | ||
} | ||
|
||
func normalizeValue(key, val string) string { | ||
switch key { | ||
case ParamRel: | ||
// [RFC 8288] requires the relation types to be compared character | ||
// by character in a case-insensitive fashion, whether they are | ||
// registered (well-known) or external (represented by URIs). Note | ||
// also [RFC 8288] recommends URIs to be all lowercase. | ||
// | ||
// [RFC 8288]: https://tools.ietf.org/html/rfc8288 | ||
vals := strings.Fields(val) | ||
for i := range vals { | ||
vals[i] = strings.ToLower(vals[i]) | ||
} | ||
return strings.Join(vals, " ") | ||
|
||
case ParamAs, ParamType: | ||
return strings.ToLower(val) | ||
|
||
case ParamCrossOrigin: | ||
if val == "" { | ||
return CrossOriginAnonymous | ||
} | ||
return strings.ToLower(val) | ||
|
||
default: | ||
return val // Do not normalize unknown parameters. | ||
} | ||
} | ||
|
||
func shouldElideValue(key, val string) bool { | ||
// Elide the value from `crossorigin="anonymous"` since the bare | ||
// `crossorigin` is presumably more popular. | ||
return key == ParamCrossOrigin && val == CrossOriginAnonymous | ||
} |
Oops, something went wrong.