249 lines
5.9 KiB
Go
249 lines
5.9 KiB
Go
// 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)
|
|
}
|