commit d543c1520355f99fc3b16d7eab51246590691e6d Author: Tom Hudson Date: Thu Dec 10 09:33:58 2015 -0500 Initial diff --git a/README.mkd b/README.mkd new file mode 100644 index 0000000..4461589 --- /dev/null +++ b/README.mkd @@ -0,0 +1,16 @@ +# Golang Link Header Parser + +Library for parsing HTTP Link headers. + +## Example + +```go +header := "; rel=\"next\", ; rel=\"last\"" +links := linkheader.Parse(header) + +for _, link := range links { + fmt.Printf("URL: %s; Rel: %s\n", link.URL, link.Rel) +} +``` + + diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..6f553f8 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,49 @@ +package linkheader_test + +import ( + "fmt" + + "github.com/tomnomnom/linkheader" +) + +func ExampleParse() { + header := "; rel=\"next\", ; rel=\"last\"" + links := linkheader.Parse(header) + + for _, link := range links { + fmt.Printf("URL: %s; Rel: %s\n", link.URL, link.Rel) + } + + // Output: + // URL: https://api.github.com/user/58276/repos?page=2; Rel: next + // URL: https://api.github.com/user/58276/repos?page=2; Rel: last +} + +func ExampleParseMultiple() { + headers := []string{ + "; rel=\"next\"", + "; rel=\"last\"", + } + links := linkheader.ParseMultiple(headers) + + for _, link := range links { + fmt.Printf("URL: %s; Rel: %s\n", link.URL, link.Rel) + } + + // Output: + // URL: https://api.github.com/user/58276/repos?page=2; Rel: next + // URL: https://api.github.com/user/58276/repos?page=2; Rel: last +} + +func ExampleFilterByRel() { + header := "; rel=\"next\", ; rel=\"last\"" + links := linkheader.Parse(header) + + for _, link := range links.FilterByRel("last") { + fmt.Printf("URL: %s; Rel: %s\n", link.URL, link.Rel) + } + + // Output: + // URL: https://api.github.com/user/58276/repos?page=2; Rel: last + +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ee04795 --- /dev/null +++ b/main.go @@ -0,0 +1,119 @@ +package linkheader + +import ( + "fmt" + "strings" +) + +// A Link is a single URL and related parameters +type Link struct { + URL string + Rel string + Params map[string]string +} + +// HasParam returns if a Link has a particular parameter or not +func (l Link) HasParam(key string) bool { + for p, _ := range l.Params { + if p == key { + return true + } + } + return false +} + +// Param returns the value of a parameter, or an error on failure +func (l Link) Param(key string) (string, error) { + for k, v := range l.Params { + if key == k { + return v, nil + } + } + return "", fmt.Errorf("Could not find param '%s'", key) +} + +// Type Links is a slice of Link structs +type Links []Link + +// FilterByRel filters a group of Links by the provided Rel attribute +func (l Links) FilterByRel(r string) Links { + links := make(Links, 0) + for _, link := range l { + if link.Rel == r { + links = append(links, link) + } + } + return links +} + +// Parse parses a raw Link header in the form: +// ; rel="foo", ; rel="bar"; wat="dis" +// returning a slice of Link structs +func Parse(raw string) Links { + links := make(Links, 0) + + // One chunk: ; rel="foo" + for _, chunk := range strings.Split(raw, ",") { + + link := Link{URL: "", Rel: "", Params: make(map[string]string)} + + // Figure out what each piece of the chunk is + for _, piece := range strings.Split(chunk, ";") { + + piece = strings.Trim(piece, " ") + if piece == "" { + continue + } + + // URL + if piece[0] == '<' && piece[len(piece)-1] == '>' { + link.URL = strings.Trim(piece, "<>") + continue + } + + // Params + key, val := parseParam(piece) + if key == "" { + continue + } + + // Special case for rel + if strings.ToLower(key) == "rel" { + link.Rel = val + } + + link.Params[key] = val + + } + + links = append(links, link) + } + + return links +} + +// ParseMultiple is like Parse, but accepts a slice of headers +// rather than just one header string +func ParseMultiple(headers []string) Links { + links := make(Links, 0) + for _, header := range headers { + links = append(links, Parse(header)...) + } + return links +} + +// parseParam takes a raw param in the form key="val" and +// returns the key and value as seperate strings +func parseParam(raw string) (key, val string) { + + parts := strings.SplitN(raw, "=", 2) + if len(parts) != 2 { + return "", "" + } + + key = parts[0] + val = strings.Trim(parts[1], "\"") + + return key, val + +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..21e05fc --- /dev/null +++ b/main_test.go @@ -0,0 +1,105 @@ +package linkheader + +import ( + "testing" +) + +func TestSimple(t *testing.T) { + // Test case stolen from https://github.com/thlorenz/parse-link-header :) + header := "; rel=\"next\", " + + "; rel=\"prev\"; pet=\"cat\", " + + "; rel=\"last\"" + + links := Parse(header) + + if len(links) != 3 { + t.Errorf("Should have been 3 links returned, got %d", len(links)) + } + + if links[0].URL != "https://api.github.com/user/9287/repos?page=3&per_page=100" { + t.Errorf("First link should have URL 'https://api.github.com/user/9287/repos?page=3&per_page=100'") + } + + if links[0].Rel != "next" { + t.Errorf("First link should have rel=\"next\"") + } + + if len(links[0].Params) != 1 { + t.Errorf("First link should have exactly 1 params, but has %d", len(links[0].Params)) + } + + if len(links[1].Params) != 2 { + t.Errorf("Second link should have exactly 2 params, but has %d", len(links[1].Params)) + } + + if links[1].Params["pet"] != "cat" { + t.Errorf("Second link's 'pet' param should be 'cat', but was %s", links[1].Params["pet"]) + } + +} + +func TestLinkMethods(t *testing.T) { + header := "; rel=\"prev\"; pet=\"cat\"" + links := Parse(header) + link := links[0] + + if !link.HasParam("rel") { + t.Errorf("Link should have param 'rel'") + } + + if link.HasParam("foo") { + t.Errorf("Link should not have param 'foo'") + } + + val, err := link.Param("pet") + if err != nil { + t.Errorf("Error value should be nil") + } + if val != "cat" { + t.Errorf("Link should have param pet=\"cat\"") + } + + val, err = link.Param("foo") + if err == nil { + t.Errorf("Error value should not be nil") + } + +} + +func testLinksMethods(t *testing.T) { + header := "; rel=\"next\", " + + "; rel=\"stylesheet\"; pet=\"cat\", " + + "; rel=\"stylesheet\"" + + links := Parse(header) + + filtered := links.FilterByRel("next") + + if filtered[0].URL != "https://api.github.com/user/9287/repos?page=3&per_page=100" { + t.Errorf("URL did not match expected") + } + + filtered = links.FilterByRel("stylesheet") + if len(filtered) != 2 { + t.Errorf("Filter for stylesheet should yield 2 results but got %d", len(filtered)) + } + + filtered = links.FilterByRel("notarel") + if len(filtered) != 0 { + t.Errorf("Filter by non-existant rel should yeild no results") + } + +} + +func testParseMultiple(t *testing.T) { + headers := []string{ + "; rel=\"next\"", + "; rel=\"last\"", + } + + links := ParseMultiple(headers) + + if len(links) != 2 { + t.Errorf("Should have returned 2 links") + } +}