1 // Copyright 2013 Joshua Tacoma. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
5 // Package uritemplates is a level 3 implementation of RFC 6570 (URI
6 // Template, http://tools.ietf.org/html/rfc6570).
7 // uritemplates does not support composite values (in Go: slices or maps)
8 // and so does not qualify as a level 4 implementation.
20 unreserved = regexp.MustCompile("[^A-Za-z0-9\\-._~]")
21 reserved = regexp.MustCompile("[^A-Za-z0-9\\-._~:/?#[\\]@!$&'()*+,;=]")
22 validname = regexp.MustCompile("^([A-Za-z0-9_\\.]|%[0-9A-Fa-f][0-9A-Fa-f])+$")
23 hex = []byte("0123456789ABCDEF")
26 func pctEncode(src []byte) []byte {
27 dst := make([]byte, len(src)*3)
28 for i, b := range src {
29 buf := dst[i*3 : i*3+3]
37 // pairWriter is a convenience struct which allows escaped and unescaped
38 // versions of the template to be written in parallel.
39 type pairWriter struct {
40 escaped, unescaped bytes.Buffer
43 // Write writes the provided string directly without any escaping.
44 func (w *pairWriter) Write(s string) {
45 w.escaped.WriteString(s)
46 w.unescaped.WriteString(s)
49 // Escape writes the provided string, escaping the string for the
51 func (w *pairWriter) Escape(s string, allowReserved bool) {
52 w.unescaped.WriteString(s)
54 w.escaped.Write(reserved.ReplaceAllFunc([]byte(s), pctEncode))
56 w.escaped.Write(unreserved.ReplaceAllFunc([]byte(s), pctEncode))
60 // Escaped returns the escaped string.
61 func (w *pairWriter) Escaped() string {
62 return w.escaped.String()
65 // Unescaped returns the unescaped string.
66 func (w *pairWriter) Unescaped() string {
67 return w.unescaped.String()
70 // A uriTemplate is a parsed representation of a URI template.
71 type uriTemplate struct {
76 // parse parses a URI template string into a uriTemplate object.
77 func parse(rawTemplate string) (*uriTemplate, error) {
78 split := strings.Split(rawTemplate, "{")
79 parts := make([]templatePart, len(split)*2-1)
80 for i, s := range split {
82 if strings.Contains(s, "}") {
83 return nil, errors.New("unexpected }")
88 subsplit := strings.Split(s, "}")
89 if len(subsplit) != 2 {
90 return nil, errors.New("malformed template")
92 expression := subsplit[0]
94 parts[i*2-1], err = parseExpression(expression)
98 parts[i*2].raw = subsplit[1]
106 type templatePart struct {
116 type templateTerm struct {
122 func parseExpression(expression string) (result templatePart, err error) {
123 switch expression[0] {
126 result.allowReserved = true
127 expression = expression[1:]
131 expression = expression[1:]
135 expression = expression[1:]
140 expression = expression[1:]
146 expression = expression[1:]
152 expression = expression[1:]
156 result.allowReserved = true
157 expression = expression[1:]
161 rawterms := strings.Split(expression, ",")
162 result.terms = make([]templateTerm, len(rawterms))
163 for i, raw := range rawterms {
164 result.terms[i], err = parseTerm(raw)
172 func parseTerm(term string) (result templateTerm, err error) {
173 // TODO(djd): Remove "*" suffix parsing once we check that no APIs have
174 // mistakenly used that attribute.
175 if strings.HasSuffix(term, "*") {
176 result.explode = true
177 term = term[:len(term)-1]
179 split := strings.Split(term, ":")
182 } else if len(split) == 2 {
183 result.name = split[0]
185 parsed, err = strconv.ParseInt(split[1], 10, 0)
186 result.truncate = int(parsed)
188 err = errors.New("multiple colons in same term")
190 if !validname.MatchString(result.name) {
191 err = errors.New("not a valid name: " + result.name)
193 if result.explode && result.truncate > 0 {
194 err = errors.New("both explode and prefix modifers on same term")
199 // Expand expands a URI template with a set of values to produce the
200 // resultant URI. Two forms of the result are returned: one with all the
201 // elements escaped, and one with the elements unescaped.
202 func (t *uriTemplate) Expand(values map[string]string) (escaped, unescaped string) {
204 for _, p := range t.parts {
207 return w.Escaped(), w.Unescaped()
210 func (tp *templatePart) expand(w *pairWriter, values map[string]string) {
216 for _, term := range tp.terms {
217 value, exists := values[term.name]
227 tp.expandString(w, term, value)
231 func (tp *templatePart) expandName(w *pairWriter, name string, empty bool) {
242 func (tp *templatePart) expandString(w *pairWriter, t templateTerm, s string) {
243 if len(s) > t.truncate && t.truncate > 0 {
246 tp.expandName(w, t.name, len(s) == 0)
247 w.Escape(s, tp.allowReserved)