uritemplates.go raw

   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 modifiers 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