Initial checkin.
Only feature so far is accept header parsing.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..48b8bf9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+vendor/
diff --git a/accept.go b/accept.go
new file mode 100644
index 0000000..ed7d16e
--- /dev/null
+++ b/accept.go
@@ -0,0 +1,175 @@
+package goscaffold
+
+import (
+ "net/http"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+var qParamRe = regexp.MustCompile("^q=([0-9\\.]+)$")
+
+type acceptCriterion struct {
+ major string
+ minor string
+ precedence float32
+ origOrder int
+ match string
+}
+type acceptCriteria []acceptCriterion
+
+/*
+SelectMediaType matches a set of candidate media types against the
+Accept header in an HTTP request. It does this using the rules from
+RFC2616 section 1.4.
+If no Accept header is present, then the first choice in the
+"choices" array is returned.
+If multiple choices match, then we will take the first one specified
+in the "Accept" header.
+If there are no compatible media types, then an empty string
+is returned.
+Only the first "Accept" header on the request is considered.
+*/
+func SelectMediaType(req *http.Request, choices []string) string {
+ hdr := req.Header.Get("Accept")
+ if hdr == "" {
+ if len(choices) >= 1 {
+ return choices[0]
+ }
+ return ""
+ }
+
+ // Parse accept header and parse the criteria
+ candidates := parseAcceptHeader(hdr)
+
+ // For each choice, assign the best match (least wildcardy)
+ matches := make(map[string]*acceptCriterion)
+ for _, choice := range choices {
+ for c := range candidates {
+ crit := candidates[c]
+ if crit.matches(choice) {
+ if matches[choice] == nil ||
+ matches[choice].level() < crit.level() {
+ matches[choice] = &acceptCriterion{
+ minor: crit.minor,
+ major: crit.major,
+ precedence: crit.precedence,
+ origOrder: crit.origOrder,
+ match: choice,
+ }
+ }
+ }
+ }
+ }
+
+ if len(matches) == 0 {
+ return ""
+ }
+
+ // Sort the matches now by precedence level and original order
+ var sortedMatches acceptCriteria
+ for _, v := range matches {
+ sortedMatches = append(sortedMatches, *v)
+ }
+ sortedMatches.sort()
+
+ return sortedMatches[0].match
+}
+
+func parseAcceptHeader(hdr string) acceptCriteria {
+ var ret acceptCriteria
+ parts := strings.Split(hdr, ",")
+ for i, part := range parts {
+ candidate := parseAcceptPart(part, i)
+ ret = append(ret, candidate)
+ }
+ return ret
+}
+
+/*
+parseAcceptPart parses a single section of the header and extracts the
+"q" parameter.
+*/
+func parseAcceptPart(part string, order int) acceptCriterion {
+ params := strings.Split(part, ";")
+ finalType := strings.TrimSpace(params[0])
+ var precedence float32 = 1.0
+
+ for _, param := range params[1:] {
+ match := qParamRe.FindStringSubmatch(strings.TrimSpace(param))
+ if match == nil {
+ finalType += ";" + param
+ } else {
+ qVal, err := strconv.ParseFloat(match[1], 32)
+ if err == nil {
+ precedence = float32(qVal)
+ }
+ }
+ }
+
+ splitType := strings.SplitN(finalType, "/", 2)
+
+ return acceptCriterion{
+ major: splitType[0],
+ minor: splitType[1],
+ precedence: precedence,
+ origOrder: order,
+ }
+}
+
+/*
+matches matches a candidate media type with a criterion.
+*/
+func (a acceptCriterion) matches(t string) bool {
+ st := strings.SplitN(t, "/", 2)
+
+ if a.major != "*" && a.major != st[0] {
+ return false
+ }
+ if a.minor != "*" && a.minor != st[1] {
+ return false
+ }
+ return true
+}
+func (a acceptCriterion) level() int {
+ if a.minor == "*" {
+ if a.major == "*" {
+ return 0
+ }
+ return 1
+ }
+ return 2
+}
+
+/*
+sortCandidates sorts accept header candidates in order of:
+1) Precedence (from the "q" parameter)
+2) Original order in accept header
+3) Stable sort otherwise
+*/
+func (c acceptCriteria) sort() {
+ sort.Stable(c)
+}
+
+func (c acceptCriteria) Less(i, j int) bool {
+ // Higher precedence goes first
+ if c[i].precedence > c[j].precedence {
+ return true
+ }
+ if c[i].precedence == c[j].precedence &&
+ c[i].origOrder < c[j].origOrder {
+ return true
+ }
+ return false
+}
+
+func (c acceptCriteria) Len() int {
+ return len(c)
+}
+
+func (c acceptCriteria) Swap(i, j int) {
+ tmp := c[i]
+ c[i] = c[j]
+ c[j] = tmp
+}
diff --git a/accept_test.go b/accept_test.go
new file mode 100644
index 0000000..2a80b21
--- /dev/null
+++ b/accept_test.go
@@ -0,0 +1,139 @@
+package goscaffold
+
+import (
+ "net/http"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Accept header tests", func() {
+ It("No nothing", func() {
+ Expect(SelectMediaType(
+ makeRequest(""), []string{})).Should(BeEmpty())
+ })
+
+ It("No header, one choice", func() {
+ Expect(SelectMediaType(
+ makeRequest(""),
+ []string{"text/plain"})).Should(Equal("text/plain"))
+ })
+
+ It("No header, two choices", func() {
+ Expect(SelectMediaType(
+ makeRequest(""),
+ []string{"text/plain", "text/xml"})).Should(Equal("text/plain"))
+ })
+
+ It("One Header, two choices", func() {
+ Expect(SelectMediaType(
+ makeRequest("application/json"),
+ []string{"text/plain", "application/json"})).Should(Equal("application/json"))
+ })
+
+ It("One Header, bad choices", func() {
+ Expect(SelectMediaType(
+ makeRequest("application/json"),
+ []string{"text/plain", "application/xml"})).Should(Equal(""))
+ })
+
+ It("audio/basic", func() {
+ req := makeRequest("audio/*; q=0.2, audio/basic")
+ Expect(SelectMediaType(req,
+ []string{"audio/mp3", "audio/basic"})).Should(Equal("audio/basic"))
+ Expect(SelectMediaType(req,
+ []string{"audio/mp3", "text/plain"})).Should(Equal("audio/mp3"))
+ Expect(SelectMediaType(req,
+ []string{"text/plain", "application/xml"})).Should(Equal(""))
+ })
+
+ It("text/plain", func() {
+ req := makeRequest("text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c")
+ Expect(SelectMediaType(req,
+ []string{"text/html", "text/x-c"})).Should(Equal("text/html"))
+ Expect(SelectMediaType(req,
+ []string{"text/x-c", "text/html"})).Should(Equal("text/html"))
+ Expect(SelectMediaType(req,
+ []string{"text/x-dvi", "text/plain"})).Should(Equal("text/x-dvi"))
+ Expect(SelectMediaType(req,
+ []string{"text/plain"})).Should(Equal("text/plain"))
+ Expect(SelectMediaType(req,
+ []string{"application/json"})).Should(Equal(""))
+ })
+
+ It("text/plain 2", func() {
+ req := makeRequest(
+ `text/*;q=0.3, text/html;q=0.7, text/html;level=1,
+ text/html;level=2;q=0.4, */*;q=0.5`)
+ Expect(SelectMediaType(req,
+ []string{"text/plain", "text/html;level=1", "text/html", "image/jpeg", "text/html;level=2", "foo/bar"})).
+ Should(Equal("text/html;level=1"))
+ Expect(SelectMediaType(req,
+ []string{"image/jpeg", "text/html", "text/plain", "text/html;level=2", "foo/bar"})).
+ Should(Equal("text/html"))
+ Expect(SelectMediaType(req,
+ []string{"image/jpeg", "text/plain", "text/html;level=2"})).
+ Should(Equal("image/jpeg"))
+ Expect(SelectMediaType(req,
+ []string{"text/plain", "text/html;level=2"})).
+ Should(Equal("text/html;level=2"))
+ })
+
+ It("Weird Browser example", func() {
+ req := makeRequest("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json")
+ Expect(SelectMediaType(req,
+ []string{"application/xml", "text/xml", "text/html", "application/json"})).
+ Should(Equal("text/html"))
+ Expect(SelectMediaType(req,
+ []string{"application/xml", "application/json"})).
+ Should(Equal("application/json"))
+ })
+
+ It("Firefox Example", func() {
+ req := makeRequest("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
+ Expect(SelectMediaType(req,
+ []string{"application/xml", "text/xml", "text/html", "text/plain", "application/json"})).
+ Should(Equal("text/html"))
+ Expect(SelectMediaType(req,
+ []string{"application/xml", "application/json"})).
+ Should(Equal("application/xml"))
+ Expect(SelectMediaType(req,
+ []string{"application/json"})).
+ Should(Equal("application/json"))
+ })
+
+ It("Parse accept part", func() {
+ ap := parseAcceptPart("text/plain", 0)
+ Expect(ap.major).Should(Equal("text"))
+ Expect(ap.minor).Should(Equal("plain"))
+ Expect(ap.precedence).Should(BeEquivalentTo(1.0))
+ ap = parseAcceptPart("text/plain;q=0.2", 0)
+ Expect(ap.major).Should(Equal("text"))
+ Expect(ap.minor).Should(Equal("plain"))
+ Expect(ap.precedence).Should(BeNumerically("~", 0.2))
+ ap = parseAcceptPart("text/plain; q=0.2", 0)
+ Expect(ap.major).Should(Equal("text"))
+ Expect(ap.minor).Should(Equal("plain"))
+ Expect(ap.precedence).Should(BeNumerically("~", 0.2))
+ ap = parseAcceptPart("text/plain;level=1", 0)
+ Expect(ap.major).Should(Equal("text"))
+ Expect(ap.minor).Should(Equal("plain;level=1"))
+ Expect(ap.precedence).Should(BeEquivalentTo(1.0))
+ ap = parseAcceptPart("text/plain;level=1;q=0.5", 0)
+ Expect(ap.major).Should(Equal("text"))
+ Expect(ap.minor).Should(Equal("plain;level=1"))
+ Expect(ap.precedence).Should(BeNumerically("~", 0.5))
+ ap = parseAcceptPart("text/plain;level=1;q=0.5;foo=bar", 0)
+ Expect(ap.major).Should(Equal("text"))
+ Expect(ap.minor).Should(Equal("plain;level=1;foo=bar"))
+ Expect(ap.precedence).Should(BeNumerically("~", 0.5))
+ })
+})
+
+func makeRequest(accept string) *http.Request {
+ return &http.Request{
+ Header: http.Header{
+ "Accept": []string{accept},
+ },
+ }
+}
diff --git a/glide.lock b/glide.lock
new file mode 100644
index 0000000..910b296
--- /dev/null
+++ b/glide.lock
@@ -0,0 +1,8 @@
+hash: 9b96c783f272b0a712add71d2d753d1eb07ba91dfe9b42e3532b8d3e3cec639c
+updated: 2016-10-04T12:46:18.025613379-07:00
+imports: []
+testImports:
+- name: github.com/onsi/ginkgo
+ version: 45a5f6ffb2a14e4f29698c16ae4c0a34018a8951
+- name: github.com/onsi/gomega
+ version: d59fa0ac68bb5dd932ee8d24eed631cdd519efc3
diff --git a/glide.yaml b/glide.yaml
new file mode 100644
index 0000000..c151fcc
--- /dev/null
+++ b/glide.yaml
@@ -0,0 +1,4 @@
+package: github.com/30x/go-scaffold
+testImport:
+- package: github.com/onsi/ginkgo
+- package: github.com/onsi/gomega
diff --git a/scaffold_main_test.go b/scaffold_main_test.go
new file mode 100644
index 0000000..a12dc50
--- /dev/null
+++ b/scaffold_main_test.go
@@ -0,0 +1,13 @@
+package goscaffold
+
+import (
+ "testing"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+func TestScaffold(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Scaffold Suite")
+}