// Copyright 2013 Joshua Tacoma. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package uritemplates is a level 3 implementation of RFC 6570 (URI // Template, http://tools.ietf.org/html/rfc6570). // uritemplates does not support composite values (in Go: slices or maps) // and so does not qualify as a level 4 implementation. package uritemplates import ( "bytes" "errors" "regexp" "strconv" "strings" ) var ( unreserved = regexp.MustCompile("[^A-Za-z0-9\\-._~]") reserved = regexp.MustCompile("[^A-Za-z0-9\\-._~:/?#[\\]@!$&'()*+,;=]") validname = regexp.MustCompile("^([A-Za-z0-9_\\.]|%[0-9A-Fa-f][0-9A-Fa-f])+$") hex = []byte("0123456789ABCDEF") ) func pctEncode(src []byte) []byte { dst := make([]byte, len(src)*3) for i, b := range src { buf := dst[i*3 : i*3+3] buf[0] = 0x25 buf[1] = hex[b/16] buf[2] = hex[b%16] } return dst } func escape(s string, allowReserved bool) string { if allowReserved { return string(reserved.ReplaceAllFunc([]byte(s), pctEncode)) } return string(unreserved.ReplaceAllFunc([]byte(s), pctEncode)) } // A uriTemplate is a parsed representation of a URI template. type uriTemplate struct { raw string parts []templatePart } // parse parses a URI template string into a uriTemplate object. func parse(rawTemplate string) (*uriTemplate, error) { split := strings.Split(rawTemplate, "{") parts := make([]templatePart, len(split)*2-1) for i, s := range split { if i == 0 { if strings.Contains(s, "}") { return nil, errors.New("unexpected }") } parts[i].raw = s continue } subsplit := strings.Split(s, "}") if len(subsplit) != 2 { return nil, errors.New("malformed template") } expression := subsplit[0] var err error parts[i*2-1], err = parseExpression(expression) if err != nil { return nil, err } parts[i*2].raw = subsplit[1] } return &uriTemplate{ raw: rawTemplate, parts: parts, }, nil } type templatePart struct { raw string terms []templateTerm first string sep string named bool ifemp string allowReserved bool } type templateTerm struct { name string explode bool truncate int } func parseExpression(expression string) (result templatePart, err error) { switch expression[0] { case '+': result.sep = "," result.allowReserved = true expression = expression[1:] case '.': result.first = "." result.sep = "." expression = expression[1:] case '/': result.first = "/" result.sep = "/" expression = expression[1:] case ';': result.first = ";" result.sep = ";" result.named = true expression = expression[1:] case '?': result.first = "?" result.sep = "&" result.named = true result.ifemp = "=" expression = expression[1:] case '&': result.first = "&" result.sep = "&" result.named = true result.ifemp = "=" expression = expression[1:] case '#': result.first = "#" result.sep = "," result.allowReserved = true expression = expression[1:] default: result.sep = "," } rawterms := strings.Split(expression, ",") result.terms = make([]templateTerm, len(rawterms)) for i, raw := range rawterms { result.terms[i], err = parseTerm(raw) if err != nil { break } } return result, err } func parseTerm(term string) (result templateTerm, err error) { // TODO(djd): Remove "*" suffix parsing once we check that no APIs have // mistakenly used that attribute. if strings.HasSuffix(term, "*") { result.explode = true term = term[:len(term)-1] } split := strings.Split(term, ":") if len(split) == 1 { result.name = term } else if len(split) == 2 { result.name = split[0] var parsed int64 parsed, err = strconv.ParseInt(split[1], 10, 0) result.truncate = int(parsed) } else { err = errors.New("multiple colons in same term") } if !validname.MatchString(result.name) { err = errors.New("not a valid name: " + result.name) } if result.explode && result.truncate > 0 { err = errors.New("both explode and prefix modifers on same term") } return result, err } // Expand expands a URI template with a set of values to produce a string. func (t *uriTemplate) Expand(values map[string]string) string { var buf bytes.Buffer for _, p := range t.parts { p.expand(&buf, values) } return buf.String() } func (tp *templatePart) expand(buf *bytes.Buffer, values map[string]string) { if len(tp.raw) > 0 { buf.WriteString(tp.raw) return } var first = true for _, term := range tp.terms { value, exists := values[term.name] if !exists { continue } if first { buf.WriteString(tp.first) first = false } else { buf.WriteString(tp.sep) } tp.expandString(buf, term, value) } } func (tp *templatePart) expandName(buf *bytes.Buffer, name string, empty bool) { if tp.named { buf.WriteString(name) if empty { buf.WriteString(tp.ifemp) } else { buf.WriteString("=") } } } func (tp *templatePart) expandString(buf *bytes.Buffer, t templateTerm, s string) { if len(s) > t.truncate && t.truncate > 0 { s = s[:t.truncate] } tp.expandName(buf, t.name, len(s) == 0) buf.WriteString(escape(s, tp.allowReserved)) }