// 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 } // pairWriter is a convenience struct which allows escaped and unescaped // versions of the template to be written in parallel. type pairWriter struct { escaped, unescaped bytes.Buffer } // Write writes the provided string directly without any escaping. func (w *pairWriter) Write(s string) { w.escaped.WriteString(s) w.unescaped.WriteString(s) } // Escape writes the provided string, escaping the string for the // escaped output. func (w *pairWriter) Escape(s string, allowReserved bool) { w.unescaped.WriteString(s) if allowReserved { w.escaped.Write(reserved.ReplaceAllFunc([]byte(s), pctEncode)) } else { w.escaped.Write(unreserved.ReplaceAllFunc([]byte(s), pctEncode)) } } // Escaped returns the escaped string. func (w *pairWriter) Escaped() string { return w.escaped.String() } // Unescaped returns the unescaped string. func (w *pairWriter) Unescaped() string { return w.unescaped.String() } // 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 the // resultant URI. Two forms of the result are returned: one with all the // elements escaped, and one with the elements unescaped. func (t *uriTemplate) Expand(values map[string]string) (escaped, unescaped string) { var w pairWriter for _, p := range t.parts { p.expand(&w, values) } return w.Escaped(), w.Unescaped() } func (tp *templatePart) expand(w *pairWriter, values map[string]string) { if len(tp.raw) > 0 { w.Write(tp.raw) return } var first = true for _, term := range tp.terms { value, exists := values[term.name] if !exists { continue } if first { w.Write(tp.first) first = false } else { w.Write(tp.sep) } tp.expandString(w, term, value) } } func (tp *templatePart) expandName(w *pairWriter, name string, empty bool) { if tp.named { w.Write(name) if empty { w.Write(tp.ifemp) } else { w.Write("=") } } } func (tp *templatePart) expandString(w *pairWriter, t templateTerm, s string) { if len(s) > t.truncate && t.truncate > 0 { s = s[:t.truncate] } tp.expandName(w, t.name, len(s) == 0) w.Escape(s, tp.allowReserved) }