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. 4 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. 9 package uritemplates 10 11 import ( 12 "bytes" 13 "errors" 14 "regexp" 15 "strconv" 16 "strings" 17 ) 18 19 var ( 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") 24 ) 25 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] 30 buf[0] = 0x25 31 buf[1] = hex[b/16] 32 buf[2] = hex[b%16] 33 } 34 return dst 35 } 36 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 41 } 42 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) 47 } 48 49 // Escape writes the provided string, escaping the string for the 50 // escaped output. 51 func (w *pairWriter) Escape(s string, allowReserved bool) { 52 w.unescaped.WriteString(s) 53 if allowReserved { 54 w.escaped.Write(reserved.ReplaceAllFunc([]byte(s), pctEncode)) 55 } else { 56 w.escaped.Write(unreserved.ReplaceAllFunc([]byte(s), pctEncode)) 57 } 58 } 59 60 // Escaped returns the escaped string. 61 func (w *pairWriter) Escaped() string { 62 return w.escaped.String() 63 } 64 65 // Unescaped returns the unescaped string. 66 func (w *pairWriter) Unescaped() string { 67 return w.unescaped.String() 68 } 69 70 // A uriTemplate is a parsed representation of a URI template. 71 type uriTemplate struct { 72 raw string 73 parts []templatePart 74 } 75 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 { 81 if i == 0 { 82 if strings.Contains(s, "}") { 83 return nil, errors.New("unexpected }") 84 } 85 parts[i].raw = s 86 continue 87 } 88 subsplit := strings.Split(s, "}") 89 if len(subsplit) != 2 { 90 return nil, errors.New("malformed template") 91 } 92 expression := subsplit[0] 93 var err error 94 parts[i*2-1], err = parseExpression(expression) 95 if err != nil { 96 return nil, err 97 } 98 parts[i*2].raw = subsplit[1] 99 } 100 return &uriTemplate{ 101 raw: rawTemplate, 102 parts: parts, 103 }, nil 104 } 105 106 type templatePart struct { 107 raw string 108 terms []templateTerm 109 first string 110 sep string 111 named bool 112 ifemp string 113 allowReserved bool 114 } 115 116 type templateTerm struct { 117 name string 118 explode bool 119 truncate int 120 } 121 122 func parseExpression(expression string) (result templatePart, err error) { 123 switch expression[0] { 124 case '+': 125 result.sep = "," 126 result.allowReserved = true 127 expression = expression[1:] 128 case '.': 129 result.first = "." 130 result.sep = "." 131 expression = expression[1:] 132 case '/': 133 result.first = "/" 134 result.sep = "/" 135 expression = expression[1:] 136 case ';': 137 result.first = ";" 138 result.sep = ";" 139 result.named = true 140 expression = expression[1:] 141 case '?': 142 result.first = "?" 143 result.sep = "&" 144 result.named = true 145 result.ifemp = "=" 146 expression = expression[1:] 147 case '&': 148 result.first = "&" 149 result.sep = "&" 150 result.named = true 151 result.ifemp = "=" 152 expression = expression[1:] 153 case '#': 154 result.first = "#" 155 result.sep = "," 156 result.allowReserved = true 157 expression = expression[1:] 158 default: 159 result.sep = "," 160 } 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) 165 if err != nil { 166 break 167 } 168 } 169 return result, err 170 } 171 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] 178 } 179 split := strings.Split(term, ":") 180 if len(split) == 1 { 181 result.name = term 182 } else if len(split) == 2 { 183 result.name = split[0] 184 var parsed int64 185 parsed, err = strconv.ParseInt(split[1], 10, 0) 186 result.truncate = int(parsed) 187 } else { 188 err = errors.New("multiple colons in same term") 189 } 190 if !validname.MatchString(result.name) { 191 err = errors.New("not a valid name: " + result.name) 192 } 193 if result.explode && result.truncate > 0 { 194 err = errors.New("both explode and prefix modifers on same term") 195 } 196 return result, err 197 } 198 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) { 203 var w pairWriter 204 for _, p := range t.parts { 205 p.expand(&w, values) 206 } 207 return w.Escaped(), w.Unescaped() 208 } 209 210 func (tp *templatePart) expand(w *pairWriter, values map[string]string) { 211 if len(tp.raw) > 0 { 212 w.Write(tp.raw) 213 return 214 } 215 var first = true 216 for _, term := range tp.terms { 217 value, exists := values[term.name] 218 if !exists { 219 continue 220 } 221 if first { 222 w.Write(tp.first) 223 first = false 224 } else { 225 w.Write(tp.sep) 226 } 227 tp.expandString(w, term, value) 228 } 229 } 230 231 func (tp *templatePart) expandName(w *pairWriter, name string, empty bool) { 232 if tp.named { 233 w.Write(name) 234 if empty { 235 w.Write(tp.ifemp) 236 } else { 237 w.Write("=") 238 } 239 } 240 } 241 242 func (tp *templatePart) expandString(w *pairWriter, t templateTerm, s string) { 243 if len(s) > t.truncate && t.truncate > 0 { 244 s = s[:t.truncate] 245 } 246 tp.expandName(w, t.name, len(s) == 0) 247 w.Escape(s, tp.allowReserved) 248 } 249