Home | History | Annotate | Download | only in uritemplates
      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