New upstream version 0.1.0+git20180905
Sophie Brun
3 years ago
0 | # Contributing | |
1 | ||
2 | * Raise an issue if appropriate | |
3 | * Fork the repo | |
4 | * Bootstrap the dev dependencies (run `./script/bootstrap`) | |
5 | * Make your changes | |
6 | * Use [gofmt](https://golang.org/cmd/gofmt/) | |
7 | * Make sure the tests pass (run `./script/test`) | |
8 | * Make sure the linters pass (run `./script/lint`) | |
9 | * Issue a pull request |
0 | MIT License | |
1 | ||
2 | Copyright (c) 2016 Tom Hudson | |
3 | ||
4 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
5 | of this software and associated documentation files (the "Software"), to deal | |
6 | in the Software without restriction, including without limitation the rights | |
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
8 | copies of the Software, and to permit persons to whom the Software is | |
9 | furnished to do so, subject to the following conditions: | |
10 | ||
11 | The above copyright notice and this permission notice shall be included in all | |
12 | copies or substantial portions of the Software. | |
13 | ||
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
20 | SOFTWARE. |
0 | # Golang Link Header Parser | |
1 | ||
2 | Library for parsing HTTP Link headers. Requires Go 1.6 or higher. | |
3 | ||
4 | Docs can be found on [the GoDoc page](https://godoc.org/github.com/tomnomnom/linkheader). | |
5 | ||
6 | [![Build Status](https://travis-ci.org/tomnomnom/linkheader.svg)](https://travis-ci.org/tomnomnom/linkheader) | |
7 | ||
8 | ## Basic Example | |
9 | ||
10 | ```go | |
11 | package main | |
12 | ||
13 | import ( | |
14 | "fmt" | |
15 | ||
16 | "github.com/tomnomnom/linkheader" | |
17 | ) | |
18 | ||
19 | func main() { | |
20 | header := "<https://api.github.com/user/58276/repos?page=2>; rel=\"next\"," + | |
21 | "<https://api.github.com/user/58276/repos?page=2>; rel=\"last\"" | |
22 | links := linkheader.Parse(header) | |
23 | ||
24 | for _, link := range links { | |
25 | fmt.Printf("URL: %s; Rel: %s\n", link.URL, link.Rel) | |
26 | } | |
27 | } | |
28 | ||
29 | // Output: | |
30 | // URL: https://api.github.com/user/58276/repos?page=2; Rel: next | |
31 | // URL: https://api.github.com/user/58276/repos?page=2; Rel: last | |
32 | ``` | |
33 | ||
34 |
0 | package linkheader_test | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | ||
5 | "github.com/tomnomnom/linkheader" | |
6 | ) | |
7 | ||
8 | func ExampleParse() { | |
9 | header := "<https://api.github.com/user/58276/repos?page=2>; rel=\"next\"," + | |
10 | "<https://api.github.com/user/58276/repos?page=2>; rel=\"last\"" | |
11 | links := linkheader.Parse(header) | |
12 | ||
13 | for _, link := range links { | |
14 | fmt.Printf("URL: %s; Rel: %s\n", link.URL, link.Rel) | |
15 | } | |
16 | ||
17 | // Output: | |
18 | // URL: https://api.github.com/user/58276/repos?page=2; Rel: next | |
19 | // URL: https://api.github.com/user/58276/repos?page=2; Rel: last | |
20 | } | |
21 | ||
22 | func ExampleParseMultiple() { | |
23 | headers := []string{ | |
24 | "<https://api.github.com/user/58276/repos?page=2>; rel=\"next\"", | |
25 | "<https://api.github.com/user/58276/repos?page=2>; rel=\"last\"", | |
26 | } | |
27 | links := linkheader.ParseMultiple(headers) | |
28 | ||
29 | for _, link := range links { | |
30 | fmt.Printf("URL: %s; Rel: %s\n", link.URL, link.Rel) | |
31 | } | |
32 | ||
33 | // Output: | |
34 | // URL: https://api.github.com/user/58276/repos?page=2; Rel: next | |
35 | // URL: https://api.github.com/user/58276/repos?page=2; Rel: last | |
36 | } | |
37 | ||
38 | func ExampleLinks_FilterByRel() { | |
39 | header := "<https://api.github.com/user/58276/repos?page=2>; rel=\"next\"," + | |
40 | "<https://api.github.com/user/58276/repos?page=2>; rel=\"last\"" | |
41 | links := linkheader.Parse(header) | |
42 | ||
43 | for _, link := range links.FilterByRel("last") { | |
44 | fmt.Printf("URL: %s; Rel: %s\n", link.URL, link.Rel) | |
45 | } | |
46 | ||
47 | // Output: | |
48 | // URL: https://api.github.com/user/58276/repos?page=2; Rel: last | |
49 | ||
50 | } | |
51 | ||
52 | func ExampleLink_String() { | |
53 | link := linkheader.Link{ | |
54 | URL: "http://example.com/page/2", | |
55 | Rel: "next", | |
56 | } | |
57 | ||
58 | fmt.Printf("Link: %s\n", link.String()) | |
59 | ||
60 | // Output: | |
61 | // Link: <http://example.com/page/2>; rel="next" | |
62 | } | |
63 | ||
64 | func ExampleLinks_String() { | |
65 | ||
66 | links := linkheader.Links{ | |
67 | {URL: "http://example.com/page/3", Rel: "next"}, | |
68 | {URL: "http://example.com/page/1", Rel: "last"}, | |
69 | } | |
70 | ||
71 | fmt.Printf("Link: %s\n", links.String()) | |
72 | ||
73 | // Output: | |
74 | // Link: <http://example.com/page/3>; rel="next", <http://example.com/page/1>; rel="last" | |
75 | } |
0 | // Package linkheader provides functions for parsing HTTP Link headers | |
1 | package linkheader | |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | "strings" | |
6 | ) | |
7 | ||
8 | // A Link is a single URL and related parameters | |
9 | type Link struct { | |
10 | URL string | |
11 | Rel string | |
12 | Params map[string]string | |
13 | } | |
14 | ||
15 | // HasParam returns if a Link has a particular parameter or not | |
16 | func (l Link) HasParam(key string) bool { | |
17 | for p := range l.Params { | |
18 | if p == key { | |
19 | return true | |
20 | } | |
21 | } | |
22 | return false | |
23 | } | |
24 | ||
25 | // Param returns the value of a parameter if it exists | |
26 | func (l Link) Param(key string) string { | |
27 | for k, v := range l.Params { | |
28 | if key == k { | |
29 | return v | |
30 | } | |
31 | } | |
32 | return "" | |
33 | } | |
34 | ||
35 | // String returns the string representation of a link | |
36 | func (l Link) String() string { | |
37 | ||
38 | p := make([]string, 0, len(l.Params)) | |
39 | for k, v := range l.Params { | |
40 | p = append(p, fmt.Sprintf("%s=\"%s\"", k, v)) | |
41 | } | |
42 | if l.Rel != "" { | |
43 | p = append(p, fmt.Sprintf("%s=\"%s\"", "rel", l.Rel)) | |
44 | } | |
45 | return fmt.Sprintf("<%s>; %s", l.URL, strings.Join(p, "; ")) | |
46 | } | |
47 | ||
48 | // Links is a slice of Link structs | |
49 | type Links []Link | |
50 | ||
51 | // FilterByRel filters a group of Links by the provided Rel attribute | |
52 | func (l Links) FilterByRel(r string) Links { | |
53 | links := make(Links, 0) | |
54 | for _, link := range l { | |
55 | if link.Rel == r { | |
56 | links = append(links, link) | |
57 | } | |
58 | } | |
59 | return links | |
60 | } | |
61 | ||
62 | // String returns the string representation of multiple Links | |
63 | // for use in HTTP responses etc | |
64 | func (l Links) String() string { | |
65 | if l == nil { | |
66 | return fmt.Sprint(nil) | |
67 | } | |
68 | ||
69 | var strs []string | |
70 | for _, link := range l { | |
71 | strs = append(strs, link.String()) | |
72 | } | |
73 | return strings.Join(strs, ", ") | |
74 | } | |
75 | ||
76 | // Parse parses a raw Link header in the form: | |
77 | // <url>; rel="foo", <url>; rel="bar"; wat="dis" | |
78 | // returning a slice of Link structs | |
79 | func Parse(raw string) Links { | |
80 | var links Links | |
81 | ||
82 | // One chunk: <url>; rel="foo" | |
83 | for _, chunk := range strings.Split(raw, ",") { | |
84 | ||
85 | link := Link{URL: "", Rel: "", Params: make(map[string]string)} | |
86 | ||
87 | // Figure out what each piece of the chunk is | |
88 | for _, piece := range strings.Split(chunk, ";") { | |
89 | ||
90 | piece = strings.Trim(piece, " ") | |
91 | if piece == "" { | |
92 | continue | |
93 | } | |
94 | ||
95 | // URL | |
96 | if piece[0] == '<' && piece[len(piece)-1] == '>' { | |
97 | link.URL = strings.Trim(piece, "<>") | |
98 | continue | |
99 | } | |
100 | ||
101 | // Params | |
102 | key, val := parseParam(piece) | |
103 | if key == "" { | |
104 | continue | |
105 | } | |
106 | ||
107 | // Special case for rel | |
108 | if strings.ToLower(key) == "rel" { | |
109 | link.Rel = val | |
110 | } else { | |
111 | link.Params[key] = val | |
112 | } | |
113 | } | |
114 | ||
115 | if link.URL != "" { | |
116 | links = append(links, link) | |
117 | } | |
118 | } | |
119 | ||
120 | return links | |
121 | } | |
122 | ||
123 | // ParseMultiple is like Parse, but accepts a slice of headers | |
124 | // rather than just one header string | |
125 | func ParseMultiple(headers []string) Links { | |
126 | links := make(Links, 0) | |
127 | for _, header := range headers { | |
128 | links = append(links, Parse(header)...) | |
129 | } | |
130 | return links | |
131 | } | |
132 | ||
133 | // parseParam takes a raw param in the form key="val" and | |
134 | // returns the key and value as seperate strings | |
135 | func parseParam(raw string) (key, val string) { | |
136 | ||
137 | parts := strings.SplitN(raw, "=", 2) | |
138 | if len(parts) == 1 { | |
139 | return parts[0], "" | |
140 | } | |
141 | if len(parts) != 2 { | |
142 | return "", "" | |
143 | } | |
144 | ||
145 | key = parts[0] | |
146 | val = strings.Trim(parts[1], "\"") | |
147 | ||
148 | return key, val | |
149 | ||
150 | } |
0 | package linkheader | |
1 | ||
2 | import "testing" | |
3 | ||
4 | func TestSimple(t *testing.T) { | |
5 | // Test case stolen from https://github.com/thlorenz/parse-link-header :) | |
6 | header := "<https://api.github.com/user/9287/repos?page=3&per_page=100>; rel=\"next\", " + | |
7 | "<https://api.github.com/user/9287/repos?page=1&per_page=100>; rel=\"prev\"; pet=\"cat\", " + | |
8 | "<https://api.github.com/user/9287/repos?page=5&per_page=100>; rel=\"last\"" | |
9 | ||
10 | links := Parse(header) | |
11 | ||
12 | if len(links) != 3 { | |
13 | t.Errorf("Should have been 3 links returned, got %d", len(links)) | |
14 | } | |
15 | ||
16 | if links[0].URL != "https://api.github.com/user/9287/repos?page=3&per_page=100" { | |
17 | t.Errorf("First link should have URL 'https://api.github.com/user/9287/repos?page=3&per_page=100'") | |
18 | } | |
19 | ||
20 | if links[0].Rel != "next" { | |
21 | t.Errorf("First link should have rel=\"next\"") | |
22 | } | |
23 | ||
24 | if len(links[0].Params) != 0 { | |
25 | t.Errorf("First link should have exactly 0 params, but has %d", len(links[0].Params)) | |
26 | } | |
27 | ||
28 | if len(links[1].Params) != 1 { | |
29 | t.Errorf("Second link should have exactly 1 params, but has %d", len(links[1].Params)) | |
30 | } | |
31 | ||
32 | if links[1].Params["pet"] != "cat" { | |
33 | t.Errorf("Second link's 'pet' param should be 'cat', but was %s", links[1].Params["pet"]) | |
34 | } | |
35 | ||
36 | } | |
37 | ||
38 | func TestEmpty(t *testing.T) { | |
39 | links := Parse("") | |
40 | if links != nil { | |
41 | t.Errorf("Return value should be nil, but was %d", len(links)) | |
42 | } | |
43 | } | |
44 | ||
45 | // Although not often seen in the wild, the grammar in RFC 5988 suggests that it's | |
46 | // valid for a link header to have nothing but a URL. | |
47 | func TestNoRel(t *testing.T) { | |
48 | links := Parse("<http://example.com>") | |
49 | ||
50 | if len(links) != 1 { | |
51 | t.Fatalf("Length of links should be 1, but was %d", len(links)) | |
52 | } | |
53 | ||
54 | if links[0].URL != "http://example.com" { | |
55 | t.Errorf("URL should be http://example.com, but was %s", links[0].URL) | |
56 | } | |
57 | } | |
58 | ||
59 | func TestLinkMethods(t *testing.T) { | |
60 | header := "<https://api.github.com/user/9287/repos?page=1&per_page=100>; rel=\"prev\"; pet=\"cat\"" | |
61 | links := Parse(header) | |
62 | link := links[0] | |
63 | ||
64 | if link.HasParam("foo") { | |
65 | t.Errorf("Link should not have param 'foo'") | |
66 | } | |
67 | ||
68 | val := link.Param("pet") | |
69 | if val != "cat" { | |
70 | t.Errorf("Link should have param pet=\"cat\"") | |
71 | } | |
72 | ||
73 | val = link.Param("foo") | |
74 | if val != "" { | |
75 | t.Errorf("Link should not have value for param 'foo'") | |
76 | } | |
77 | ||
78 | } | |
79 | ||
80 | func TestLinksMethods(t *testing.T) { | |
81 | header := "<https://api.github.com/user/9287/repos?page=3&per_page=100>; rel=\"next\", " + | |
82 | "<https://api.github.com/user/9287/repos?page=1&per_page=100>; rel=\"stylesheet\"; pet=\"cat\", " + | |
83 | "<https://api.github.com/user/9287/repos?page=5&per_page=100>; rel=\"stylesheet\"" | |
84 | ||
85 | links := Parse(header) | |
86 | ||
87 | filtered := links.FilterByRel("next") | |
88 | ||
89 | if filtered[0].URL != "https://api.github.com/user/9287/repos?page=3&per_page=100" { | |
90 | t.Errorf("URL did not match expected") | |
91 | } | |
92 | ||
93 | filtered = links.FilterByRel("stylesheet") | |
94 | if len(filtered) != 2 { | |
95 | t.Errorf("Filter for stylesheet should yield 2 results but got %d", len(filtered)) | |
96 | } | |
97 | ||
98 | filtered = links.FilterByRel("notarel") | |
99 | if len(filtered) != 0 { | |
100 | t.Errorf("Filter by non-existant rel should yeild no results") | |
101 | } | |
102 | ||
103 | } | |
104 | ||
105 | func TestParseMultiple(t *testing.T) { | |
106 | headers := []string{ | |
107 | "<https://api.github.com/user/58276/repos?page=2>; rel=\"next\"", | |
108 | "<https://api.github.com/user/58276/repos?page=2>; rel=\"last\"", | |
109 | } | |
110 | ||
111 | links := ParseMultiple(headers) | |
112 | ||
113 | if len(links) != 2 { | |
114 | t.Errorf("Should have returned 2 links") | |
115 | } | |
116 | } | |
117 | ||
118 | func TestLinkToString(t *testing.T) { | |
119 | l := Link{ | |
120 | URL: "http://example.com/page/2", | |
121 | Rel: "next", | |
122 | Params: map[string]string{ | |
123 | "foo": "bar", | |
124 | }, | |
125 | } | |
126 | ||
127 | have := l.String() | |
128 | ||
129 | parsed := Parse(have) | |
130 | ||
131 | if len(parsed) != 1 { | |
132 | t.Errorf("Expected only 1 link") | |
133 | } | |
134 | ||
135 | if parsed[0].URL != l.URL { | |
136 | t.Errorf("Re-parsed link header should have matching URL, but has `%s`", parsed[0].URL) | |
137 | } | |
138 | ||
139 | if parsed[0].Rel != l.Rel { | |
140 | t.Errorf("Re-parsed link header should have matching rel, but has `%s`", parsed[0].Rel) | |
141 | } | |
142 | ||
143 | if parsed[0].Param("foo") != "bar" { | |
144 | t.Errorf("Re-parsed link header should have foo=\"bar\" but doesn't") | |
145 | } | |
146 | } | |
147 | ||
148 | func TestLinksToString(t *testing.T) { | |
149 | ls := Links{ | |
150 | {URL: "http://example.com/page/3", Rel: "next"}, | |
151 | {URL: "http://example.com/page/1", Rel: "last"}, | |
152 | } | |
153 | ||
154 | have := ls.String() | |
155 | ||
156 | want := "<http://example.com/page/3>; rel=\"next\", <http://example.com/page/1>; rel=\"last\"" | |
157 | ||
158 | if have != want { | |
159 | t.Errorf("Want `%s`, have `%s`", want, have) | |
160 | } | |
161 | } | |
162 | ||
163 | func BenchmarkParse(b *testing.B) { | |
164 | ||
165 | header := "<https://api.github.com/user/9287/repos?page=3&per_page=100>; rel=\"next\", " + | |
166 | "<https://api.github.com/user/9287/repos?page=1&per_page=100>; rel=\"prev\"; pet=\"cat\", " + | |
167 | "<https://api.github.com/user/9287/repos?page=5&per_page=100>; rel=\"last\"" | |
168 | ||
169 | for i := 0; i < b.N; i++ { | |
170 | _ = Parse(header) | |
171 | } | |
172 | } |
0 | #!/bin/sh | |
1 | PROJDIR=$(cd `dirname $0`/.. && pwd) | |
2 | ||
3 | echo "Installing gometalinter and linters..." | |
4 | go get github.com/alecthomas/gometalinter | |
5 | gometalinter --install |