pax_global_header00006660000000000000000000000064127004225050014507gustar00rootroot0000000000000052 comment=d1cebc7ea14a5fc0de7cb4a45acae773161642c6 link-1.0.0/000077500000000000000000000000001270042250500124425ustar00rootroot00000000000000link-1.0.0/.travis.yml000066400000000000000000000000571270042250500145550ustar00rootroot00000000000000language: go go: - 1.6 - 1.5.3 sudo: false link-1.0.0/README.md000066400000000000000000000065161270042250500137310ustar00rootroot00000000000000# link [![Build Status](https://travis-ci.org/peterhellberg/link.svg?branch=master)](https://travis-ci.org/peterhellberg/link) [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/peterhellberg/link) [![License MIT](https://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)](https://github.com/peterhellberg/link#license-mit) Parses **Link** headers used for pagination, as defined in [RFC 5988](https://tools.ietf.org/html/rfc5988). This package was originally based on , but **Parse** takes a `string` instead of `*http.Request` in this version. It also has the convenience functions **ParseHeader**, **ParseRequest** and **ParseResponse**. ## Installation go get -u github.com/peterhellberg/link ## Exported functions - [Parse(s string) Group](https://godoc.org/github.com/peterhellberg/link#Parse) - [ParseHeader(h http.Header) Group](https://godoc.org/github.com/peterhellberg/link#ParseHeader) - [ParseRequest(req \*http.Request) Group](https://godoc.org/github.com/peterhellberg/link#ParseRequest) - [ParseResponse(resp \*http.Response) Group](https://godoc.org/github.com/peterhellberg/link#ParseResponse) ## Usage ```go package main import ( "fmt" "net/http" "github.com/peterhellberg/link" ) func main() { for _, l := range link.Parse(`; rel="next"; foo="bar"`) { fmt.Printf("URI: %q, Rel: %q, Extra: %+v\n", l.URI, l.Rel, l.Extra) // URI: "https://example.com/?page=2", Rel: "next", Extra: map[foo:bar] } if resp, err := http.Get("https://api.github.com/search/code?q=Println+user:golang"); err == nil { for _, l := range link.ParseResponse(resp) { fmt.Printf("URI: %q, Rel: %q, Extra: %+v\n", l.URI, l.Rel, l.Extra) // URI: "https://api.github.com/search/code?q=Println+user%3Agolang&page=2", Rel: "next", Extra: map[] // URI: "https://api.github.com/search/code?q=Println+user%3Agolang&page=34", Rel: "last", Extra: map[] } } } ``` ## Not supported - Extended notation ([RFC 5987](https://tools.ietf.org/html/rfc5987)) ## Alternatives to this package - [github.com/tent/http-link-go](https://github.com/tent/http-link-go) - [github.com/swhite24/link](https://github.com/swhite24/link) ## License (MIT) Copyright (c) 2015-2016 [Peter Hellberg](http://c7.se/) > Permission is hereby granted, free of charge, to any person obtaining > a copy of this software and associated documentation files (the > "Software"), to deal in the Software without restriction, including > without limitation the rights to use, copy, modify, merge, publish, > distribute, sublicense, and/or sell copies of the Software, and to > permit persons to whom the Software is furnished to do so, subject to > the following conditions: > The above copyright notice and this permission notice shall be > included in all copies or substantial portions of the Software. > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION > OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. link-1.0.0/doc.go000066400000000000000000000020431270042250500135350ustar00rootroot00000000000000/* Package link parses Link headers used for pagination, as defined in RFC 5988 Installation Just go get the package: go get -u github.com/peterhellberg/link Usage A small usage example package main import ( "fmt" "net/http" "github.com/peterhellberg/link" ) func main() { for _, l := range link.Parse(`; rel="next"; foo="bar"`) { fmt.Printf("URI: %q, Rel: %q, Extra: %+v\n", l.URI, l.Rel, l.Extra) // URI: "https://example.com/?page=2", Rel: "next", Extra: map[foo:bar] } if resp, err := http.Get("https://api.github.com/search/code?q=Println+user:golang"); err == nil { for _, l := range link.ParseResponse(resp) { fmt.Printf("URI: %q, Rel: %q, Extra: %+v\n", l.URI, l.Rel, l.Extra) // URI: "https://api.github.com/search/code?q=Println+user%3Agolang&page=2", Rel: "next", Extra: map[] // URI: "https://api.github.com/search/code?q=Println+user%3Agolang&page=34", Rel: "last", Extra: map[] } } } */ package link link-1.0.0/link.go000066400000000000000000000044561270042250500137370ustar00rootroot00000000000000package link import ( "net/http" "regexp" "strings" ) var ( commaRegexp = regexp.MustCompile(`,\s{0,}`) valueCommaRegexp = regexp.MustCompile(`([^"]),`) equalRegexp = regexp.MustCompile(` *= *`) keyRegexp = regexp.MustCompile(`[a-z*]+`) linkRegexp = regexp.MustCompile(`\A<(.+)>;(.+)\z`) semiRegexp = regexp.MustCompile(`; +`) valRegexp = regexp.MustCompile(`"+([^"]+)"+`) ) // Group returned by Parse, contains multiple links indexed by "rel" type Group map[string]*Link // Link contains a Link item with URI, Rel, and other non-URI components in Extra. type Link struct { URI string Rel string Extra map[string]string } // String returns the URI func (l *Link) String() string { return l.URI } // ParseRequest parses the provided *http.Request into a Group func ParseRequest(req *http.Request) Group { if req == nil { return nil } return ParseHeader(req.Header) } // ParseResponse parses the provided *http.Response into a Group func ParseResponse(resp *http.Response) Group { if resp == nil { return nil } return ParseHeader(resp.Header) } // ParseHeader retrieves the Link header from the provided http.Header and parses it into a Group func ParseHeader(h http.Header) Group { if headers, found := h["Link"]; found { return Parse(strings.Join(headers, ", ")) } return nil } // Parse parses the provided string into a Group func Parse(s string) Group { if s == "" { return nil } s = valueCommaRegexp.ReplaceAllString(s, "$1") group := Group{} for _, l := range commaRegexp.Split(s, -1) { linkMatches := linkRegexp.FindAllStringSubmatch(l, -1) if len(linkMatches) == 0 { return nil } pieces := linkMatches[0] link := &Link{URI: pieces[1], Extra: map[string]string{}} for _, extra := range semiRegexp.Split(pieces[2], -1) { vals := equalRegexp.Split(extra, -1) key := keyRegexp.FindString(vals[0]) val := valRegexp.FindStringSubmatch(vals[1])[1] if key == "rel" { vals := strings.Split(val, " ") rels := []string{vals[0]} if len(vals) > 1 { for _, v := range vals[1:] { if !strings.HasPrefix(v, "http") { rels = append(rels, v) } } } rel := strings.Join(rels, " ") link.Rel = rel group[rel] = link } else { link.Extra[key] = val } } } return group } link-1.0.0/link_test.go000066400000000000000000000173261270042250500147760ustar00rootroot00000000000000package link import ( "fmt" "net/http" "testing" ) func TestLinkString(t *testing.T) { l := Parse(`; rel="next"; title="foo"`)["next"] if got, want := l.String(), "https://example.com/?page=2"; got != want { t.Fatalf(`l.String() = %q, want %q`, got, want) } } func TestParseRequest(t *testing.T) { req, _ := http.NewRequest("GET", "", nil) req.Header.Set("Link", `; rel="next"`) g := ParseRequest(req) if got, want := len(g), 1; got != want { t.Fatalf(`len(g) = %d, want %d`, got, want) } if g["next"] == nil { t.Fatalf(`g["next"] == nil`) } if got, want := g["next"].URI, "https://example.com/?page=2"; got != want { t.Fatalf(`g["next"].URI = %q, want %q`, got, want) } if got, want := g["next"].Rel, "next"; got != want { t.Fatalf(`g["next"].Rel = %q, want %q`, got, want) } if got, want := len(ParseRequest(nil)), 0; got != want { t.Fatalf(`len(ParseRequest(nil)) = %d, want %d`, got, want) } } func TestParseResponse(t *testing.T) { resp := &http.Response{Header: http.Header{}} resp.Header.Set("Link", `; rel="next"`) g := ParseResponse(resp) if got, want := len(g), 1; got != want { t.Fatalf(`len(g) = %d, want %d`, got, want) } if g["next"] == nil { t.Fatalf(`g["next"] == nil`) } if got, want := g["next"].URI, "https://example.com/?page=2"; got != want { t.Fatalf(`g["next"].URI = %q, want %q`, got, want) } if got, want := g["next"].Rel, "next"; got != want { t.Fatalf(`g["next"].Rel = %q, want %q`, got, want) } if got, want := len(ParseResponse(nil)), 0; got != want { t.Fatalf(`len(ParseResponse(nil)) = %d, want %d`, got, want) } } func TestParseHeader_single(t *testing.T) { h := http.Header{} h.Set("Link", `; rel="next"`) g := ParseHeader(h) if got, want := len(g), 1; got != want { t.Fatalf(`len(g) = %d, want %d`, got, want) } if g["next"] == nil { t.Fatalf(`g["next"] == nil`) } if got, want := g["next"].URI, "https://example.com/?page=2"; got != want { t.Fatalf(`g["next"].URI = %q, want %q`, got, want) } if got, want := g["next"].Rel, "next"; got != want { t.Fatalf(`g["next"].Rel = %q, want %q`, got, want) } } func TestParseHeader_multiple(t *testing.T) { h := http.Header{} h.Add("Link", `; rel="next",; rel="last"`) g := ParseHeader(h) if got, want := len(g), 2; got != want { t.Fatalf(`len(g) = %d, want %d`, got, want) } if g["next"] == nil { t.Fatalf(`g["next"] == nil`) } if got, want := g["next"].URI, "https://example.com/?page=2"; got != want { t.Fatalf(`g["next"].URI = %q, want %q`, got, want) } if got, want := g["next"].Rel, "next"; got != want { t.Fatalf(`g["next"].Rel = %q, want %q`, got, want) } if g["last"] == nil { t.Fatalf(`g["last"] == nil`) } if got, want := g["last"].URI, "https://example.com/?page=34"; got != want { t.Fatalf(`g["last"].URI = %q, want %q`, got, want) } if got, want := g["last"].Rel, "last"; got != want { t.Fatalf(`g["last"].Rel = %q, want %q`, got, want) } } func TestParseHeader_multiple_headers(t *testing.T) { h := http.Header{} h.Add("Link", `; rel="next",; rel="last"`) h.Add("Link", `; rel="foo",; rel="bar"`) g := ParseHeader(h) if got, want := len(g), 4; got != want { t.Fatalf(`len(g) = %d, want %d`, got, want) } if g["foo"] == nil { t.Fatalf(`g["foo"] == nil`) } if got, want := g["bar"].URI, "https://example.com/?page=bar"; got != want { t.Fatalf(`g["bar"].URI = %q, want %q`, got, want) } if got, want := g["next"].Rel, "next"; got != want { t.Fatalf(`g["next"].Rel = %q, want %q`, got, want) } if g["last"] == nil { t.Fatalf(`g["last"] == nil`) } if got, want := g["last"].URI, "https://example.com/?page=34"; got != want { t.Fatalf(`g["last"].URI = %q, want %q`, got, want) } if got, want := g["last"].Rel, "last"; got != want { t.Fatalf(`g["last"].Rel = %q, want %q`, got, want) } } func TestParseHeader_extra(t *testing.T) { h := http.Header{} h.Add("Link", `; rel="next"; title="foo"`) g := ParseHeader(h) if got, want := len(g), 1; got != want { t.Fatalf(`len(g) = %d, want %d`, got, want) } if g["next"] == nil { t.Fatalf(`g["next"] == nil`) } if got, want := g["next"].Extra["title"], "foo"; got != want { t.Fatalf(`g["next"].Extra["title"] = %q, want %q`, got, want) } } func TestParseHeader_noLink(t *testing.T) { if ParseHeader(http.Header{}) != nil { t.Fatalf(`Parse(http.Header{}) != nil`) } } func TestParseHeader_nilHeader(t *testing.T) { if ParseHeader(nil) != nil { t.Fatalf(`ParseHeader(nil) != nil`) } } func TestParse_emptyString(t *testing.T) { if Parse("") != nil { t.Fatalf(`Parse("") != nil`) } } func TestParse_valuesWithComma(t *testing.T) { g := Parse(`; rel="original latest-version",; rel="timegate",; rel="timemap"; type="application/link-format"; from="Mon, 03 Sep 2007 14:52:48 GMT"; until="Tue, 16 Jun 2015 22:59:23 GMT",; rel="first memento"; datetime="Mon, 03 Sep 2007 14:52:48 GMT",; rel="last memento"; datetime="Tue, 16 Jun 2015 22:59:23 GMT"`) if got, want := len(g), 5; got != want { t.Fatalf(`len(g) = %d, want %d`, got, want) } if got, want := g["original latest-version"].URI, "//www.w3.org/wiki/LinkHeader"; got != want { t.Fatalf(`g["original latest-version"].URI = %q, want %q`, got, want) } if got, want := g["last memento"].Extra["datetime"], "Tue 16 Jun 2015 22:59:23 GMT"; got != want { t.Fatalf(`g["last memento"].Extra["datetime"] = %q, want %q`, got, want) } } func TestParse_rfc5988Example1(t *testing.T) { g := Parse(`; rel="previous"; title="previous chapter"`) if got, want := len(g), 1; got != want { t.Fatalf(`len(g) = %d, want %d`, got, want) } if g["previous"] == nil { t.Fatalf(`g["previous"] == nil`) } if got, want := g["previous"].Extra["title"], "previous chapter"; got != want { t.Fatalf(`g["previous"].Extra["title"] = %q, want %q`, got, want) } } func TestParse_rfc5988Example2(t *testing.T) { g := Parse(`; rel="http://example.net/foo"`) if got, want := len(g), 1; got != want { t.Fatalf(`len(g) = %d, want %d`, got, want) } if g["http://example.net/foo"] == nil { t.Fatalf(`g["http://example.net/foo"] == nil`) } l := g["http://example.net/foo"] if got, want := l.URI, "/"; got != want { t.Fatalf(`l.URI = %q, want %q`, got, want) } } func TestParse_rfc5988Example3(t *testing.T) { // Extended notation is not supported yet // g := Parse(`; rel="previous"; title*=UTF-8'de'letztes%20Kapitel, ; rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel`) } func TestParse_rfc5988Example4(t *testing.T) { // Extension relation types are ignored for now g := Parse(`; rel="start http://example.net/relation/other"`) if got, want := len(g), 1; got != want { t.Fatalf(`len(g) = %d, want %d`, got, want) } if g["start"] == nil { t.Fatalf(`g["start"] == nil`) } if got, want := g["start"].URI, "http://example.org/"; got != want { t.Fatalf(`g["start"].URI = %q, want %q`, got, want) } } func TestParse_fuzzCrashers(t *testing.T) { Parse("0") } func ExampleParse() { l := Parse(`; rel="next"; title="foo"`)["next"] fmt.Printf("URI: %q, Rel: %q, Extra: %+v\n", l.URI, l.Rel, l.Extra) // Output: URI: "https://example.com/?page=2", Rel: "next", Extra: map[title:foo] }