diff --git a/burrito.cabal b/burrito.cabal
--- a/burrito.cabal
+++ b/burrito.cabal
@@ -1,5 +1,5 @@
 name: burrito
-version: 1.0.2.0
+version: 1.1.0.0
 
 synopsis: Parse and render URI templates.
 description:
@@ -34,31 +34,43 @@
 library
   build-depends:
     base >= 4.12.0 && < 4.15
+    , bytestring >= 0.10.8 && < 0.11
+    , containers >= 0.6.0 && < 0.7
+    , parsec >= 3.1.14 && < 3.2
     , template-haskell >= 2.14.0 && < 2.17
+    , text >= 1.2.3 && < 1.3
+    , transformers >= 0.5.6 && < 0.6
   default-language: Haskell98
   exposed-modules:
     Burrito
-    Burrito.Expand
-    Burrito.Parse
-    Burrito.Render
-    Burrito.TH
-    Burrito.Type.Expression
-    Burrito.Type.LitChar
-    Burrito.Type.Literal
-    Burrito.Type.Modifier
-    Burrito.Type.Name
-    Burrito.Type.NonEmpty
-    Burrito.Type.Operator
-    Burrito.Type.Template
-    Burrito.Type.Token
-    Burrito.Type.Value
-    Burrito.Type.VarChar
-    Burrito.Type.Variable
+    Burrito.Internal.Expand
+    Burrito.Internal.Match
+    Burrito.Internal.Parse
+    Burrito.Internal.Render
+    Burrito.Internal.TH
+    Burrito.Internal.Type.Case
+    Burrito.Internal.Type.Character
+    Burrito.Internal.Type.Digit
+    Burrito.Internal.Type.Expression
+    Burrito.Internal.Type.Field
+    Burrito.Internal.Type.Literal
+    Burrito.Internal.Type.Match
+    Burrito.Internal.Type.MaxLength
+    Burrito.Internal.Type.Modifier
+    Burrito.Internal.Type.Name
+    Burrito.Internal.Type.Operator
+    Burrito.Internal.Type.Template
+    Burrito.Internal.Type.Token
+    Burrito.Internal.Type.Value
+    Burrito.Internal.Type.Variable
   ghc-options:
     -Weverything
     -Wno-all-missed-specialisations
     -Wno-implicit-prelude
+    -Wno-missing-export-lists
     -Wno-missing-exported-signatures
+    -Wno-missing-local-signatures
+    -Wno-monomorphism-restriction
     -Wno-safe
   hs-source-dirs: src/lib
 
@@ -75,8 +87,10 @@
   build-depends:
     base -any
     , burrito -any
+    , containers -any
     , hspec >= 2.7.1 && < 2.8
     , QuickCheck >= 2.13.2 && < 2.14
+    , text -any
   default-language: Haskell98
   hs-source-dirs: src/test
   main-is: Main.hs
diff --git a/src/lib/Burrito.hs b/src/lib/Burrito.hs
--- a/src/lib/Burrito.hs
+++ b/src/lib/Burrito.hs
@@ -2,8 +2,8 @@
 --
 -- According to [RFC 6570](https://tools.ietf.org/html/rfc6570): "A URI
 -- Template is a compact sequence of characters for describing a range of
--- Uniform Resource Identifiers through variable expansion." Burrito implements
--- URI templates according to the specification in that RFC.
+  -- Uniform Resource Identifiers through variable expansion." Burrito
+  -- implements URI templates according to the specification in that RFC.
 --
 -- The term "uniform resource identifiers" (URI) is often used interchangeably
 -- with other related terms like "internationalized resource identifier" (IRI),
@@ -30,6 +30,8 @@
   ( Parse.parse
   , Render.render
   , Expand.expand
+  , Expand.expandWith
+  , Match.match
   , TH.uriTemplate
   , Template.Template
   , Value.Value
@@ -39,24 +41,26 @@
   )
 where
 
-import qualified Burrito.Expand as Expand
-import qualified Burrito.Parse as Parse
-import qualified Burrito.Render as Render
-import qualified Burrito.TH as TH
-import qualified Burrito.Type.Template as Template
-import qualified Burrito.Type.Value as Value
-
+import qualified Burrito.Internal.Expand as Expand
+import qualified Burrito.Internal.Match as Match
+import qualified Burrito.Internal.Parse as Parse
+import qualified Burrito.Internal.Render as Render
+import qualified Burrito.Internal.TH as TH
+import qualified Burrito.Internal.Type.Template as Template
+import qualified Burrito.Internal.Type.Value as Value
+import qualified Data.Bifunctor as Bifunctor
+import qualified Data.Map as Map
+import qualified Data.Text as Text
 
 -- | Constructs a string value.
 stringValue :: String -> Value.Value
-stringValue = Value.String
-
+stringValue = Value.String . Text.pack
 
 -- | Constructs a list value.
 listValue :: [String] -> Value.Value
-listValue = Value.List
-
+listValue = Value.List . fmap Text.pack
 
 -- | Constructs a dictionary value.
 dictionaryValue :: [(String, String)] -> Value.Value
-dictionaryValue = Value.Dictionary
+dictionaryValue =
+  Value.Dictionary . Map.fromList . fmap (Bifunctor.bimap Text.pack Text.pack)
diff --git a/src/lib/Burrito/Expand.hs b/src/lib/Burrito/Expand.hs
deleted file mode 100644
--- a/src/lib/Burrito/Expand.hs
+++ /dev/null
@@ -1,442 +0,0 @@
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Expand
-  ( expand
-  )
-where
-
-import qualified Burrito.Type.Expression as Expression
-import qualified Burrito.Type.LitChar as LitChar
-import qualified Burrito.Type.Literal as Literal
-import qualified Burrito.Type.Modifier as Modifier
-import qualified Burrito.Type.Name as Name
-import qualified Burrito.Type.NonEmpty as NonEmpty
-import qualified Burrito.Type.Operator as Operator
-import qualified Burrito.Type.Template as Template
-import qualified Burrito.Type.Token as Token
-import qualified Burrito.Type.Value as Value
-import qualified Burrito.Type.VarChar as VarChar
-import qualified Burrito.Type.Variable as Variable
-import qualified Data.Bits as Bits
-import qualified Data.Char as Char
-import qualified Data.Functor.Identity as Identity
-import qualified Data.List as List
-import qualified Data.Maybe as Maybe
-import qualified Data.Word as Word
-import qualified Text.Printf as Printf
-
-
--- | Expands a template using the given values. Unlike parsing, expansion
--- always succeeds. If no value is given for a variable, it will simply not
--- appear in the output.
-expand :: [(String, Value.Value)] -> Template.Template -> String
-expand values = Identity.runIdentity
-  . expandTemplate (pure . flip lookup values . expandName)
-
-
--- | Expands a template for output according to section 3 of the RFC, using the
--- given function to resolve variable values.
-expandTemplate
-  :: Applicative m
-  => (Name.Name -> m (Maybe Value.Value))
-  -> Template.Template
-  -> m String
-expandTemplate f = expandTokens f . Template.tokens
-
-
--- | Expands tokens for output according to section 3 of the RFC, using the
--- given function to resolve variable values.
-expandTokens
-  :: Applicative m
-  => (Name.Name -> m (Maybe Value.Value))
-  -> [Token.Token]
-  -> m String
-expandTokens f = fmap concat . traverse (expandToken f)
-
-
--- | Expands a token for output according to section 3 of the RFC, using the
--- given function to resolve variable values.
-expandToken
-  :: Applicative m
-  => (Name.Name -> m (Maybe Value.Value))
-  -> Token.Token
-  -> m String
-expandToken f token = case token of
-  Token.Literal literal -> pure $ expandLiteral literal
-  Token.Expression expression -> expandExpression f expression
-
-
--- | Expands a literal token for output according to section 3.1 of the RFC.
-expandLiteral :: Literal.Literal -> String
-expandLiteral =
-  concatMap expandCharacter . NonEmpty.toList . Literal.characters
-
-
--- | Expands a single literal character for output. This is necessary to
--- normalize percent encodings and to encode characters that aren't allowed to
--- appear in URIs.
-expandCharacter :: LitChar.LitChar -> String
-expandCharacter character = case character of
-  LitChar.Encoded word8 -> percentEncodeWord8 word8
-  LitChar.Unencoded char -> escapeChar Operator.PlusSign char
-
-
--- | If necessary, escapes a character for output with the given operator.
--- Otherwise returns the character unchanged as a string.
-escapeChar :: Operator.Operator -> Char -> String
-escapeChar operator char =
-  if isAllowed operator char then [char] else percentEncodeChar char
-
-
--- | Returns true if the given character is allowed unescaped in the output for
--- the given operator.
-isAllowed :: Operator.Operator -> Char -> Bool
-isAllowed operator char = case operator of
-  Operator.NumberSign -> isUnreserved char || isReserved char
-  Operator.PlusSign -> isUnreserved char || isReserved char
-  _ -> isUnreserved char
-
-
--- | Percent encodes a character by UTF-8 encoding it and then percent encoding
--- the resulting octets.
-percentEncodeChar :: Char -> String
-percentEncodeChar = concatMap percentEncodeWord8 . encodeUtf8
-
-
--- | Percent encodes an octet by converting it into uppercase hexadecimal
--- digits and prepending a percent sign. For example @12@ becomes @"%0C"@.
-percentEncodeWord8 :: Word.Word8 -> String
-percentEncodeWord8 = Printf.printf "%%%02X"
-
-
--- | Expands an expression for output according to section 3.2 of the RFC,
--- using the given function to resolve variable values.
-expandExpression
-  :: Applicative m
-  => (Name.Name -> m (Maybe Value.Value))
-  -> Expression.Expression
-  -> m String
-expandExpression f expression =
-  let
-    operator = Expression.operator expression
-    prefix = prefixFor operator
-    separator = separatorFor operator
-    finalize expansions =
-      (if null expansions then "" else prefix)
-        <> List.intercalate separator expansions
-  in fmap finalize . expandVariables f operator $ Expression.variables
-    expression
-
-
--- | Returns the prefix to use before an expression for the given operator.
-prefixFor :: Operator.Operator -> String
-prefixFor operator = case operator of
-  Operator.Ampersand -> "&"
-  Operator.FullStop -> "."
-  Operator.None -> ""
-  Operator.NumberSign -> "#"
-  Operator.PlusSign -> ""
-  Operator.QuestionMark -> "?"
-  Operator.Semicolon -> ";"
-  Operator.Solidus -> "/"
-
-
--- | Returns the separator to use between values for the given operator.
-separatorFor :: Operator.Operator -> String
-separatorFor operator = case operator of
-  Operator.Ampersand -> "&"
-  Operator.FullStop -> "."
-  Operator.None -> ","
-  Operator.NumberSign -> ","
-  Operator.PlusSign -> ","
-  Operator.QuestionMark -> "&"
-  Operator.Semicolon -> ";"
-  Operator.Solidus -> "/"
-
-
--- | Expands variables for output according to section 3.2 of the RFC, using
--- the given function to resolve variable values.
-expandVariables
-  :: Applicative m
-  => (Name.Name -> m (Maybe Value.Value))
-  -> Operator.Operator
-  -> NonEmpty.NonEmpty Variable.Variable
-  -> m [String]
-expandVariables f operator =
-  fmap Maybe.catMaybes . traverse (expandVariable f operator) . NonEmpty.toList
-
-
--- | Expands a variable for output according to section 3.2.1 of the RFC, using
--- the given function to resolve variable values.
-expandVariable
-  :: Applicative m
-  => (Name.Name -> m (Maybe Value.Value))
-  -> Operator.Operator
-  -> Variable.Variable
-  -> m (Maybe String)
-expandVariable f operator variable =
-  let
-    name = Variable.name variable
-    modifier = Variable.modifier variable
-  in expandMaybeValue operator name modifier <$> f name
-
-
--- | If the given value is not nothing, expand it according to section 3.2.1 of
--- the RFC.
-expandMaybeValue
-  :: Operator.Operator
-  -> Name.Name
-  -> Modifier.Modifier
-  -> Maybe Value.Value
-  -> Maybe String
-expandMaybeValue operator name modifier maybeValue = do
-  value <- maybeValue
-  expandValue operator name modifier value
-
-
--- | Expands a value for output according to section 3.2.1 of the RFC. If the
--- value is undefined according to section 2.3, this returns nothing.
-expandValue
-  :: Operator.Operator
-  -> Name.Name
-  -> Modifier.Modifier
-  -> Value.Value
-  -> Maybe String
-expandValue operator name modifier value = case value of
-  Value.Dictionary dictionary ->
-    expandDictionary operator name modifier <$> NonEmpty.fromList dictionary
-  Value.List list ->
-    expandList operator name modifier <$> NonEmpty.fromList list
-  Value.String string -> Just $ expandString operator name modifier string
-
-
--- | Expands a dictionary (associative array) value for output.
-expandDictionary
-  :: Operator.Operator
-  -> Name.Name
-  -> Modifier.Modifier
-  -> NonEmpty.NonEmpty (String, String)
-  -> String
-expandDictionary = expandElements
-  $ \operator _ modifier -> expandDictionaryElement operator modifier
-
-
--- | Expands one element of a dictionary value for output.
-expandDictionaryElement
-  :: Operator.Operator -> Modifier.Modifier -> (String, String) -> [String]
-expandDictionaryElement operator modifier (name, value) =
-  let escape = escapeString operator Modifier.None
-  in
-    case modifier of
-      Modifier.Asterisk -> [escape name <> "=" <> escape value]
-      _ -> [escape name, escape value]
-
-
--- | Expands a list value for output.
-expandList
-  :: Operator.Operator
-  -> Name.Name
-  -> Modifier.Modifier
-  -> NonEmpty.NonEmpty String
-  -> String
-expandList = expandElements $ \operator name modifier ->
-  pure . expandListElement operator name modifier
-
-
--- | Expands one element of a list value for output.
-expandListElement
-  :: Operator.Operator -> Name.Name -> Modifier.Modifier -> String -> String
-expandListElement operator name modifier = case modifier of
-  Modifier.Asterisk -> expandString operator name Modifier.None
-  _ -> expandString Operator.None name Modifier.None
-
-
--- | Expands a collection of elements for output. This is used for both
--- dictionaries and lists.
-expandElements
-  :: (Operator.Operator -> Name.Name -> Modifier.Modifier -> a -> [String])
-  -> Operator.Operator
-  -> Name.Name
-  -> Modifier.Modifier
-  -> NonEmpty.NonEmpty a
-  -> String
-expandElements f operator name modifier =
-  let
-    showPrefix = case modifier of
-      Modifier.Asterisk -> False
-      _ -> case operator of
-        Operator.Ampersand -> True
-        Operator.QuestionMark -> True
-        Operator.Semicolon -> True
-        _ -> False
-    prefix = if showPrefix then expandName name <> "=" else ""
-    separator = case modifier of
-      Modifier.Asterisk -> separatorFor operator
-      _ -> ","
-  in
-    mappend prefix
-    . List.intercalate separator
-    . concatMap (f operator name modifier)
-    . NonEmpty.toList
-
-
--- | Expands a string value for output.
-expandString
-  :: Operator.Operator -> Name.Name -> Modifier.Modifier -> String -> String
-expandString operator name modifier s =
-  let
-    prefix = case operator of
-      Operator.Ampersand -> expandName name <> "="
-      Operator.QuestionMark -> expandName name <> "="
-      Operator.Semicolon -> expandName name <> if null s then "" else "="
-      _ -> ""
-  in prefix <> escapeString operator modifier s
-
-
--- | Escapes a string value for output. This handles encoding characters as
--- necessary for the given oeprator, as well as taking the prefix as necessary
--- for the given modifier.
-escapeString :: Operator.Operator -> Modifier.Modifier -> String -> String
-escapeString operator modifier string =
-  concatMap (escapeChar operator) $ case modifier of
-    Modifier.Colon size -> take size string
-    _ -> string
-
-
--- | Expands a variable name for output.
-expandName :: Name.Name -> String
-expandName name = mconcat
-  [ expandVarChar $ Name.first name
-  , concatMap
-      (\(fullStop, varChar) ->
-        (if fullStop then "." else "") <> expandVarChar varChar
-      )
-    $ Name.rest name
-  ]
-
-
--- | Expands a single logical character of a variable name for output.
-expandVarChar :: VarChar.VarChar -> String
-expandVarChar varChar = case varChar of
-  VarChar.Encoded hi lo -> ['%', hi, lo]
-  VarChar.Unencoded char -> [char]
-
-
--- | Encodes a character as a series of UTF-8 octets. The resulting list will
--- have between one and four elements.
-encodeUtf8 :: Char -> [Word.Word8]
-encodeUtf8 char =
-  let
-    oneByte x = [intToWord8 $ bitAnd 0x7f x]
-    twoBytes x =
-      [ bitOr 0xc0 . intToWord8 . bitAnd 0x3f $ bitShiftR 6 x
-      , bitOr 0x80 . intToWord8 $ bitAnd 0x3f x
-      ]
-    threeBytes x =
-      [ bitOr 0xe0 . intToWord8 . bitAnd 0x0f $ bitShiftR 12 x
-      , bitOr 0x80 . intToWord8 . bitAnd 0x3f $ bitShiftR 6 x
-      , bitOr 0x80 . intToWord8 $ bitAnd 0x3f x
-      ]
-    fourBytes x =
-      [ bitOr 0xf0 . intToWord8 . bitAnd 0x07 $ bitShiftR 18 x
-      , bitOr 0x80 . intToWord8 . bitAnd 0x3f $ bitShiftR 12 x
-      , bitOr 0x80 . intToWord8 . bitAnd 0x3f $ bitShiftR 6 x
-      , bitOr 0x80 . intToWord8 $ bitAnd 0x3f x
-      ]
-  in case Char.ord char of
-    int
-      | int <= 0x7f -> oneByte int
-      | int <= 0x7ff -> twoBytes int
-      | int <= 0xffff -> threeBytes int
-      | otherwise -> fourBytes int
-
-
--- | Computes the bitwise AND of the two parameters.
-bitAnd :: Bits.Bits a => a -> a -> a
-bitAnd = (Bits..&.)
-
-
--- | Computes the bitwise OR of the two parameters.
-bitOr :: Bits.Bits a => a -> a -> a
-bitOr = (Bits..|.)
-
-
--- | Shifts the given value to the right by the specified number of bits. If
--- the shift amount is negative, an exception will be thrown.
-bitShiftR :: Bits.Bits a => Int -> a -> a
-bitShiftR = flip Bits.shiftR
-
-
--- | Converts a machine-sized signed integer into an eight-bit unsigned
--- integer. If the input is out of bounds, an exception will be thrown.
-intToWord8 :: Int -> Word.Word8
-intToWord8 x =
-  let
-    lo = word8ToInt (minBound :: Word.Word8)
-    hi = word8ToInt (maxBound :: Word.Word8)
-  in if x < lo
-    then error $ "intToWord8: " <> show x <> " < " <> show lo
-    else if x > hi
-      then error $ "intToWord8: " <> show x <> " > " <> show hi
-      else fromIntegral x
-
-
--- | Converts an eight-bit unsigned integer into a machine-sized signed
--- integer. This conversion cannot fail.
-word8ToInt :: Word.Word8 -> Int
-word8ToInt = fromIntegral
-
-
--- | Returns true if the given character is in the @ALPHA@ range defined by
--- section 1.5 of the RFC.
-isAlpha :: Char -> Bool
-isAlpha x = Char.isAsciiUpper x || Char.isAsciiLower x
-
-
--- | Returns true if the given character is in the @reserved@ range defined by
--- section 1.5 of the RFC.
-isReserved :: Char -> Bool
-isReserved x = isGenDelim x || isSubDelim x
-
-
--- | Returns true if the given character is in the @gen-delims@ range defined
--- by section 1.5 of the RFC.
-isGenDelim :: Char -> Bool
-isGenDelim x = case x of
-  ':' -> True
-  '/' -> True
-  '?' -> True
-  '#' -> True
-  '[' -> True
-  ']' -> True
-  '@' -> True
-  _ -> False
-
-
--- | Returns true if the given character is in the @sub-delims@ range defined
--- by section 1.5 of the RFC.
-isSubDelim :: Char -> Bool
-isSubDelim x = case x of
-  '!' -> True
-  '$' -> True
-  '&' -> True
-  '\'' -> True
-  '(' -> True
-  ')' -> True
-  '*' -> True
-  '+' -> True
-  ',' -> True
-  ';' -> True
-  '=' -> True
-  _ -> False
-
-
--- | Returns true if the given character is in the @unreserved@ range defined
--- by section 1.5 of the RFC.
-isUnreserved :: Char -> Bool
-isUnreserved x = case x of
-  '-' -> True
-  '.' -> True
-  '_' -> True
-  '~' -> True
-  _ -> isAlpha x || Char.isDigit x
diff --git a/src/lib/Burrito/Internal/Expand.hs b/src/lib/Burrito/Internal/Expand.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Expand.hs
@@ -0,0 +1,310 @@
+module Burrito.Internal.Expand where
+
+import qualified Burrito.Internal.Render as Render
+import qualified Burrito.Internal.Type.Character as Character
+import qualified Burrito.Internal.Type.Digit as Digit
+import qualified Burrito.Internal.Type.Expression as Expression
+import qualified Burrito.Internal.Type.Field as Field
+import qualified Burrito.Internal.Type.Literal as Literal
+import qualified Burrito.Internal.Type.MaxLength as MaxLength
+import qualified Burrito.Internal.Type.Modifier as Modifier
+import qualified Burrito.Internal.Type.Name as Name
+import qualified Burrito.Internal.Type.Operator as Operator
+import qualified Burrito.Internal.Type.Template as Template
+import qualified Burrito.Internal.Type.Token as Token
+import qualified Burrito.Internal.Type.Value as Value
+import qualified Burrito.Internal.Type.Variable as Variable
+import qualified Control.Monad.Trans.Class as Trans
+import qualified Control.Monad.Trans.State as State
+import qualified Data.ByteString as ByteString
+import qualified Data.Char as Char
+import qualified Data.Functor.Identity as Identity
+import qualified Data.List as List
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map as Map
+import qualified Data.Maybe as Maybe
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Data.Text.Lazy as LazyText
+import qualified Data.Text.Lazy.Builder as Builder
+
+-- | Expands a template using the given values. Unlike parsing, expansion
+-- always succeeds. If no value is given for a variable, it will simply not
+-- appear in the output.
+--
+-- >>> expand [] <$> parse "valid-template"
+-- Just "valid-template"
+-- >>> expand [] <$> parse "template:{example}"
+-- Just "template:"
+-- >>> expand [("example", stringValue "true")] <$> parse "template:{example}"
+-- Just "template:true"
+expand :: [(String, Value.Value)] -> Template.Template -> String
+expand values =
+  let m = Map.mapKeys Text.pack $ Map.fromList values
+  in
+    Render.builderToString . Identity.runIdentity . expandWith
+      (pure . flip Map.lookup m)
+
+-- | This is like @expand@ except that it gives you more control over how
+-- variables are expanded. If you can, use @expand@. It's simpler.
+--
+-- Instead of passing in a static mapping form names to
+-- values, you pass in a function that is used to look up values on the fly.
+-- This can be useful if computing values takes a while or requires some impure
+-- actions.
+--
+-- >>> expandWith (\ x -> [Nothing, Just . stringValue $ unpack x]) <$> parse "template:{example}"
+-- Just ["template:","template:example"]
+-- >>> let Just template = parse "user={USER}"
+-- >>> expandWith (fmap (fmap stringValue) . lookupEnv . unpack) template
+-- "user=taylor"
+--
+-- Note that as the RFC specifies, the given function will be called at most
+-- once for each variable in the template.
+--
+-- >>> let Just template = parse "{a}{a}"
+-- >>> expandWith (\ x -> do { putStrLn $ "-- expanding " <> show x; pure . Just $ Burrito.stringValue "A" }) template
+-- -- expanding "a"
+-- "AA"
+expandWith
+  :: Monad m
+  => (Text.Text -> m (Maybe Value.Value))
+  -> Template.Template
+  -> m Builder.Builder
+expandWith f = flip State.evalStateT Map.empty . template (cached f)
+
+type CacheT = State.StateT (Map.Map Text.Text (Maybe Value.Value))
+
+cached
+  :: Monad m
+  => (Text.Text -> m (Maybe Value.Value))
+  -> Name.Name
+  -> CacheT m (Maybe Value.Value)
+cached f x = do
+  let key = LazyText.toStrict . Builder.toLazyText $ name x
+  cache <- State.get
+  case Map.lookup key cache of
+    Just result -> pure result
+    Nothing -> do
+      result <- Trans.lift $ f key
+      State.modify $ Map.insert key result
+      pure result
+
+template
+  :: Monad m
+  => (Name.Name -> CacheT m (Maybe Value.Value))
+  -> Template.Template
+  -> CacheT m Builder.Builder
+template f = fmap mconcat . traverse (token f) . Template.tokens
+
+token
+  :: Monad m
+  => (Name.Name -> CacheT m (Maybe Value.Value))
+  -> Token.Token
+  -> CacheT m Builder.Builder
+token f x = case x of
+  Token.Expression y -> expression f y
+  Token.Literal y -> pure $ literal y
+
+expression
+  :: Monad m
+  => (Name.Name -> CacheT m (Maybe Value.Value))
+  -> Expression.Expression
+  -> CacheT m Builder.Builder
+expression f ex =
+  let op = Expression.operator ex
+  in
+    fmap
+      (mconcat
+      . (\xs -> if null xs then xs else prefix op : xs)
+      . List.intersperse (separator op)
+      . Maybe.catMaybes
+      )
+    . traverse (variable f op)
+    . NonEmpty.toList
+    $ Expression.variables ex
+
+separator :: Operator.Operator -> Builder.Builder
+separator op = Builder.singleton $ case op of
+  Operator.Ampersand -> '&'
+  Operator.FullStop -> '.'
+  Operator.None -> ','
+  Operator.NumberSign -> ','
+  Operator.PlusSign -> ','
+  Operator.QuestionMark -> '&'
+  Operator.Semicolon -> ';'
+  Operator.Solidus -> '/'
+
+prefix :: Operator.Operator -> Builder.Builder
+prefix op = case op of
+  Operator.Ampersand -> Builder.singleton '&'
+  Operator.FullStop -> Builder.singleton '.'
+  Operator.None -> mempty
+  Operator.NumberSign -> Builder.singleton '#'
+  Operator.PlusSign -> mempty
+  Operator.QuestionMark -> Builder.singleton '?'
+  Operator.Semicolon -> Builder.singleton ';'
+  Operator.Solidus -> Builder.singleton '/'
+
+variable
+  :: Monad m
+  => (Name.Name -> CacheT m (Maybe Value.Value))
+  -> Operator.Operator
+  -> Variable.Variable
+  -> CacheT m (Maybe Builder.Builder)
+variable f op var = do
+  res <- f $ Variable.name var
+  pure $ case res of
+    Nothing -> Nothing
+    Just val -> value op var val
+
+value
+  :: Operator.Operator
+  -> Variable.Variable
+  -> Value.Value
+  -> Maybe Builder.Builder
+value op var val = case val of
+  Value.Dictionary xs -> dictionaryValue op var $ Map.toAscList xs
+  Value.List xs -> listValue op var xs
+  Value.String x -> Just $ stringValue op var x
+
+dictionaryValue
+  :: Operator.Operator
+  -> Variable.Variable
+  -> [(Text.Text, Text.Text)]
+  -> Maybe Builder.Builder
+dictionaryValue = items $ \op var (k, v) ->
+  let f = string op Modifier.None
+  in
+    case Variable.modifier var of
+      Modifier.Asterisk -> [f k <> Builder.singleton '=' <> f v]
+      _ -> [f k, f v]
+
+listValue
+  :: Operator.Operator
+  -> Variable.Variable
+  -> [Text.Text]
+  -> Maybe Builder.Builder
+listValue = items $ \op var -> pure . stringValue
+  (case Variable.modifier var of
+    Modifier.Asterisk -> op
+    _ -> Operator.None
+  )
+  var { Variable.modifier = Modifier.None }
+
+items
+  :: (Operator.Operator -> Variable.Variable -> a -> [Builder.Builder])
+  -> Operator.Operator
+  -> Variable.Variable
+  -> [a]
+  -> Maybe Builder.Builder
+items f op var xs =
+  let
+    md = Variable.modifier var
+    sep = case md of
+      Modifier.Asterisk -> separator op
+      _ -> Builder.singleton ','
+    p = case md of
+      Modifier.Asterisk -> False
+      _ -> case op of
+        Operator.Ampersand -> True
+        Operator.QuestionMark -> True
+        Operator.Semicolon -> True
+        _ -> False
+  in if null xs
+    then Nothing
+    else
+      Just
+      . mconcat
+      . (if p then (label True var :) else id)
+      . List.intersperse sep
+      $ concatMap (f op var) xs
+
+label :: Bool -> Variable.Variable -> Builder.Builder
+label p v =
+  name (Variable.name v) <> if p then Builder.singleton '=' else mempty
+
+name :: Name.Name -> Builder.Builder
+name =
+  mconcat
+    . List.intersperse (Builder.singleton '.')
+    . fmap field
+    . NonEmpty.toList
+    . Name.fields
+
+field :: Field.Field -> Builder.Builder
+field = foldMap (character $ const True) . Field.characters
+
+character :: (Char -> Bool) -> Character.Character tag -> Builder.Builder
+character f x = case x of
+  Character.Encoded y z -> Render.encodedCharacter y z
+  Character.Unencoded y -> unencodedCharacter f y
+
+stringValue
+  :: Operator.Operator -> Variable.Variable -> Text.Text -> Builder.Builder
+stringValue op var str =
+  let
+    pre = case op of
+      Operator.Ampersand -> label True var
+      Operator.QuestionMark -> label True var
+      Operator.Semicolon -> label (not $ Text.null str) var
+      _ -> mempty
+  in pre <> string op (Variable.modifier var) str
+
+string
+  :: Operator.Operator -> Modifier.Modifier -> Text.Text -> Builder.Builder
+string op md =
+  let
+    allowed x = case op of
+      Operator.NumberSign -> isAllowed x
+      Operator.PlusSign -> isAllowed x
+      _ -> isUnreserved x
+    trim = case md of
+      Modifier.Colon ml -> Text.take $ MaxLength.count ml
+      _ -> id
+  in foldMap (unencodedCharacter allowed) . Text.unpack . trim
+
+isAllowed :: Char -> Bool
+isAllowed x = isUnreserved x || isReserved x
+
+isUnreserved :: Char -> Bool
+isUnreserved x = case x of
+  '-' -> True
+  '.' -> True
+  '_' -> True
+  '~' -> True
+  _ -> Char.isAsciiUpper x || Char.isAsciiLower x || Char.isDigit x
+
+isReserved :: Char -> Bool
+isReserved x = case x of
+  '!' -> True
+  '$' -> True
+  '&' -> True
+  '\'' -> True
+  '(' -> True
+  ')' -> True
+  '*' -> True
+  '+' -> True
+  ',' -> True
+  ';' -> True
+  '=' -> True
+  ':' -> True
+  '/' -> True
+  '?' -> True
+  '#' -> True
+  '[' -> True
+  ']' -> True
+  '@' -> True
+  _ -> False
+
+unencodedCharacter :: (Char -> Bool) -> Char -> Builder.Builder
+unencodedCharacter f x = if f x
+  then Builder.singleton x
+  else foldMap (uncurry Render.encodedCharacter) $ encodeCharacter x
+
+encodeCharacter :: Char -> [(Digit.Digit, Digit.Digit)]
+encodeCharacter =
+  fmap Digit.fromWord8 . ByteString.unpack . Text.encodeUtf8 . Text.singleton
+
+literal :: Literal.Literal -> Builder.Builder
+literal = foldMap (character isAllowed) . Literal.characters
diff --git a/src/lib/Burrito/Internal/Match.hs b/src/lib/Burrito/Internal/Match.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Match.hs
@@ -0,0 +1,271 @@
+module Burrito.Internal.Match where
+
+import qualified Burrito.Internal.Expand as Expand
+import qualified Burrito.Internal.Render as Render
+import qualified Burrito.Internal.Type.Case as Case
+import qualified Burrito.Internal.Type.Character as Character
+import qualified Burrito.Internal.Type.Digit as Digit
+import qualified Burrito.Internal.Type.Expression as Expression
+import qualified Burrito.Internal.Type.Literal as Literal
+import qualified Burrito.Internal.Type.Match as Match
+import qualified Burrito.Internal.Type.MaxLength as MaxLength
+import qualified Burrito.Internal.Type.Modifier as Modifier
+import qualified Burrito.Internal.Type.Name as Name
+import qualified Burrito.Internal.Type.Operator as Operator
+import qualified Burrito.Internal.Type.Template as Template
+import qualified Burrito.Internal.Type.Token as Token
+import qualified Burrito.Internal.Type.Value as Value
+import qualified Burrito.Internal.Type.Variable as Variable
+import qualified Control.Monad as Monad
+import qualified Data.ByteString as ByteString
+import qualified Data.Char as Char
+import qualified Data.List as List
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Maybe as Maybe
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Text.ParserCombinators.ReadP as ReadP
+
+-- | Matches a string against a template. This is essentially the opposite of
+-- @expand@.
+--
+-- Since there isn't always one unique match, this function returns all the
+-- possibilities. It's up to you to select the one that makes the most sense,
+-- or to simply grab the first one if you don't care.
+--
+-- >>> match "" <$> parse "no-match"
+-- Just []
+-- >>> match "no-variables" <$> parse "no-variables"
+-- Just [[]]
+-- >>> match "1-match" <$> parse "{one}-match"
+-- Just [[("one",String "1")]]
+--
+-- Be warned that the number of possible matches can grow quickly if your
+-- template has variables next to each other without any separators.
+--
+-- >>> let Just template = parse "{a}{b}"
+-- >>> mapM_ print $ match "ab" template
+-- [("a",String "a"),("b",String "b")]
+-- [("a",String "ab"),("b",String "")]
+-- [("a",String "ab")]
+-- [("a",String ""),("b",String "ab")]
+-- [("b",String "ab")]
+--
+-- Matching supports everything /except/ explode modifiers (@{a*}@), list
+-- values, and dictionary values.
+match :: String -> Template.Template -> [[(String, Value.Value)]]
+match s =
+  fmap finalize
+    . Maybe.mapMaybe (keepConsistent . fst)
+    . flip ReadP.readP_to_S s
+    . template
+
+finalize :: [(Name.Name, Match.Match)] -> [(String, Value.Value)]
+finalize = Maybe.mapMaybe $ \(n, m) -> case m of
+  Match.Defined v ->
+    Just (Render.builderToString $ Render.name n, Value.String v)
+  Match.Prefix _ v ->
+    Just (Render.builderToString $ Render.name n, Value.String v)
+  Match.Undefined -> Nothing
+
+keepConsistent
+  :: [(Name.Name, Match.Match)] -> Maybe [(Name.Name, Match.Match)]
+keepConsistent xs = case xs of
+  [] -> Just xs
+  (k, v) : ys -> do
+    let (ts, fs) = List.partition ((== k) . fst) ys
+    w <- combine v $ fmap snd ts
+    ((k, w) :) <$> keepConsistent fs
+
+combine :: Match.Match -> [Match.Match] -> Maybe Match.Match
+combine x ys = case ys of
+  [] -> Just x
+  y : zs -> case x of
+    Match.Defined t -> case y of
+      Match.Defined u | t == u -> combine x zs
+      Match.Prefix m u | Text.take (MaxLength.count m) t == u -> combine x zs
+      _ -> Nothing
+    Match.Prefix n t -> case y of
+      Match.Defined u | t == Text.take (MaxLength.count n) u -> combine y zs
+      Match.Prefix m u
+        | let c = MaxLength.count (min n m) in Text.take c t == Text.take c u
+        -> combine (if m > n then y else x) zs
+      _ -> Nothing
+    Match.Undefined -> case y of
+      Match.Undefined -> combine x zs
+      _ -> Nothing
+
+template :: Template.Template -> ReadP.ReadP [(Name.Name, Match.Match)]
+template x = do
+  xs <- fmap mconcat . traverse token $ Template.tokens x
+  ReadP.eof
+  pure xs
+
+token :: Token.Token -> ReadP.ReadP [(Name.Name, Match.Match)]
+token x = case x of
+  Token.Expression y -> expression y
+  Token.Literal y -> [] <$ literal y
+
+expression :: Expression.Expression -> ReadP.ReadP [(Name.Name, Match.Match)]
+expression x = variables (Expression.operator x) (Expression.variables x)
+
+variables
+  :: Operator.Operator
+  -> NonEmpty.NonEmpty Variable.Variable
+  -> ReadP.ReadP [(Name.Name, Match.Match)]
+variables op vs = case op of
+  Operator.Ampersand -> vars vs (Just '&') '&' varEq
+  Operator.FullStop -> vars vs (Just '.') '.' $ variable Expand.isUnreserved
+  Operator.None -> vars vs Nothing ',' $ variable Expand.isUnreserved
+  Operator.NumberSign -> vars vs (Just '#') ',' $ variable Expand.isAllowed
+  Operator.PlusSign -> vars vs Nothing ',' $ variable Expand.isAllowed
+  Operator.QuestionMark -> vars vs (Just '?') '&' varEq
+  Operator.Semicolon -> vars vs (Just ';') ';' $ \v -> do
+    let n = Variable.name v
+    name n
+    ReadP.option [(n, Match.Defined Text.empty)] $ do
+      char_ '='
+      variable Expand.isUnreserved v
+  Operator.Solidus -> vars vs (Just '/') '/' $ variable Expand.isUnreserved
+
+vars
+  :: NonEmpty.NonEmpty Variable.Variable
+  -> Maybe Char
+  -> Char
+  -> (Variable.Variable -> ReadP.ReadP [(Name.Name, Match.Match)])
+  -> ReadP.ReadP [(Name.Name, Match.Match)]
+vars vs m c f = do
+  let
+    ctx = case m of
+      Nothing -> id
+      Just o -> \p -> ReadP.option (undef <$> NonEmpty.toList vs) $ do
+        char_ o
+        xs <- p
+        Monad.guard . not $ all isUndefined xs
+        pure xs
+  ctx . vars' c f $ NonEmpty.toList vs
+
+isUndefined :: (Name.Name, Match.Match) -> Bool
+isUndefined = (== Match.Undefined) . snd
+
+vars'
+  :: Char
+  -> (Variable.Variable -> ReadP.ReadP [(Name.Name, Match.Match)])
+  -> [Variable.Variable]
+  -> ReadP.ReadP [(Name.Name, Match.Match)]
+vars' c f vs = case vs of
+  [] -> pure []
+  v : ws ->
+    let
+      this = do
+        x <- f v
+        xs <- ReadP.option (undef <$> ws) $ do
+          char_ c
+          vars' c f ws
+        pure $ x <> xs
+      that = (undef v :) <$> vars' c f ws
+    in this ReadP.+++ that
+
+undef :: Variable.Variable -> (Name.Name, Match.Match)
+undef v = (Variable.name v, Match.Undefined)
+
+char_ :: Char -> ReadP.ReadP ()
+char_ = Monad.void . ReadP.char
+
+varEq :: Variable.Variable -> ReadP.ReadP [(Name.Name, Match.Match)]
+varEq v = do
+  name $ Variable.name v
+  char_ '='
+  variable Expand.isUnreserved v
+
+name :: Name.Name -> ReadP.ReadP ()
+name = Monad.void . ReadP.string . Render.builderToString . Render.name
+
+variable
+  :: (Char -> Bool)
+  -> Variable.Variable
+  -> ReadP.ReadP [(Name.Name, Match.Match)]
+variable f x = do
+  v <- case Variable.modifier x of
+    Modifier.Asterisk -> ReadP.pfail
+    Modifier.None -> Match.Defined <$> manyCharacters f
+    Modifier.Colon n -> Match.Prefix n <$> manyCharacters f
+  pure [(Variable.name x, v)]
+
+manyCharacters :: (Char -> Bool) -> ReadP.ReadP Text.Text
+manyCharacters f = do
+  let
+    f1 = (:) <$> someEncodedCharacters <*> ReadP.option [] f2
+    f2 = (:) <$> someUnencodedCharacters f <*> ReadP.option [] f1
+  fmap mconcat . ReadP.option [] $ f1 ReadP.<++ f2
+
+someEncodedCharacters :: ReadP.ReadP Text.Text
+someEncodedCharacters = do
+  xs <- some anEncodedCharacter
+  either (fail . show) pure
+    . Text.decodeUtf8'
+    . ByteString.pack
+    . fmap (uncurry Digit.toWord8)
+    $ NonEmpty.toList xs
+
+some :: ReadP.ReadP a -> ReadP.ReadP (NonEmpty.NonEmpty a)
+some p = (NonEmpty.:|) <$> p <*> ReadP.many p
+
+someUnencodedCharacters :: (Char -> Bool) -> ReadP.ReadP Text.Text
+someUnencodedCharacters f = do
+  xs <- some $ ReadP.satisfy f
+  pure . Text.pack $ NonEmpty.toList xs
+
+anEncodedCharacter :: ReadP.ReadP (Digit.Digit, Digit.Digit)
+anEncodedCharacter = do
+  char_ '%'
+  (,) <$> aDigit <*> aDigit
+
+aDigit :: ReadP.ReadP Digit.Digit
+aDigit = do
+  x <- ReadP.satisfy Char.isHexDigit
+  maybe (fail "invalid Digit") pure $ Digit.fromChar x
+
+literal :: Literal.Literal -> ReadP.ReadP ()
+literal = mapM_ literalCharacter . Literal.characters
+
+literalCharacter :: Character.Character Literal.Literal -> ReadP.ReadP ()
+literalCharacter = character Expand.isAllowed
+
+character :: (Char -> Bool) -> Character.Character tag -> ReadP.ReadP ()
+character f x = case x of
+  Character.Encoded y z -> encodedCharacter y z
+  Character.Unencoded y -> unencodedCharacter f y
+
+encodedCharacter :: Digit.Digit -> Digit.Digit -> ReadP.ReadP ()
+encodedCharacter x y = char_ '%' *> digit x *> digit y
+
+digit :: Digit.Digit -> ReadP.ReadP ()
+digit x = char_ $ case x of
+  Digit.Ox0 -> '0'
+  Digit.Ox1 -> '1'
+  Digit.Ox2 -> '2'
+  Digit.Ox3 -> '3'
+  Digit.Ox4 -> '4'
+  Digit.Ox5 -> '5'
+  Digit.Ox6 -> '6'
+  Digit.Ox7 -> '7'
+  Digit.Ox8 -> '8'
+  Digit.Ox9 -> '9'
+  Digit.OxA Case.Upper -> 'A'
+  Digit.OxB Case.Upper -> 'B'
+  Digit.OxC Case.Upper -> 'C'
+  Digit.OxD Case.Upper -> 'D'
+  Digit.OxE Case.Upper -> 'E'
+  Digit.OxF Case.Upper -> 'F'
+  Digit.OxA Case.Lower -> 'a'
+  Digit.OxB Case.Lower -> 'b'
+  Digit.OxC Case.Lower -> 'c'
+  Digit.OxD Case.Lower -> 'd'
+  Digit.OxE Case.Lower -> 'e'
+  Digit.OxF Case.Lower -> 'f'
+
+unencodedCharacter :: (Char -> Bool) -> Char -> ReadP.ReadP ()
+unencodedCharacter f x = if f x
+  then char_ x
+  else mapM_ (uncurry encodedCharacter) $ Expand.encodeCharacter x
diff --git a/src/lib/Burrito/Internal/Parse.hs b/src/lib/Burrito/Internal/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Parse.hs
@@ -0,0 +1,173 @@
+{-# LANGUAGE FlexibleContexts #-}
+
+module Burrito.Internal.Parse where
+
+import qualified Burrito.Internal.Type.Character as Character
+import qualified Burrito.Internal.Type.Digit as Digit
+import qualified Burrito.Internal.Type.Expression as Expression
+import qualified Burrito.Internal.Type.Field as Field
+import qualified Burrito.Internal.Type.Literal as Literal
+import qualified Burrito.Internal.Type.MaxLength as MaxLength
+import qualified Burrito.Internal.Type.Modifier as Modifier
+import qualified Burrito.Internal.Type.Name as Name
+import qualified Burrito.Internal.Type.Operator as Operator
+import qualified Burrito.Internal.Type.Template as Template
+import qualified Burrito.Internal.Type.Token as Token
+import qualified Burrito.Internal.Type.Variable as Variable
+import qualified Data.Char as Char
+import qualified Data.Ix as Ix
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Text.Parsec as Parsec
+import qualified Text.Read as Read
+
+-- | Attempts to parse a string as a URI template. If parsing fails, this will
+-- return @Nothing@. Otherwise it will return @Just@ the parsed template.
+--
+-- Parsing will usually succeed, but it can fail if the input string contains
+-- characters that are not valid in IRIs (like @^@) or if the input string
+-- contains an invalid template expression (like @{!}@). To include characters
+-- that aren't valid in IRIs, percent encode them (like @%5E@).
+--
+-- >>> parse "invalid template"
+-- Nothing
+-- >>> parse "valid-template"
+-- Just (Template ...)
+parse :: String -> Maybe Template.Template
+parse = either (const Nothing) Just . Parsec.parse template ""
+
+template :: Parsec.Stream s m Char => Parsec.ParsecT s u m Template.Template
+template = Template.Template <$> Parsec.many token <* Parsec.eof
+
+token :: Parsec.Stream s m Char => Parsec.ParsecT s u m Token.Token
+token = choice (Token.Expression <$> expression) (Token.Literal <$> literal)
+
+choice
+  :: Parsec.ParsecT s u m a -> Parsec.ParsecT s u m a -> Parsec.ParsecT s u m a
+choice = (Parsec.<|>)
+
+expression
+  :: Parsec.Stream s m Char => Parsec.ParsecT s u m Expression.Expression
+expression =
+  Parsec.between (Parsec.char '{') (Parsec.char '}')
+    $ Expression.Expression
+    <$> operator
+    <*> sepBy1 variable (Parsec.char ',')
+
+operator :: Parsec.Stream s m Char => Parsec.ParsecT s u m Operator.Operator
+operator = Parsec.option Operator.None $ Parsec.choice
+  [ Operator.Ampersand <$ Parsec.char '&'
+  , Operator.FullStop <$ Parsec.char '.'
+  , Operator.NumberSign <$ Parsec.char '#'
+  , Operator.PlusSign <$ Parsec.char '+'
+  , Operator.QuestionMark <$ Parsec.char '?'
+  , Operator.Semicolon <$ Parsec.char ';'
+  , Operator.Solidus <$ Parsec.char '/'
+  ]
+
+sepBy1
+  :: Parsec.ParsecT s u m a
+  -> Parsec.ParsecT s u m x
+  -> Parsec.ParsecT s u m (NonEmpty.NonEmpty a)
+sepBy1 p s = (NonEmpty.:|) <$> p <*> Parsec.many (s *> p)
+
+variable :: Parsec.Stream s m Char => Parsec.ParsecT s u m Variable.Variable
+variable = Variable.Variable <$> name <*> modifier
+
+name :: Parsec.Stream s m Char => Parsec.ParsecT s u m Name.Name
+name = Name.Name <$> sepBy1 field (Parsec.char '.')
+
+field :: Parsec.Stream s m Char => Parsec.ParsecT s u m Field.Field
+field = Field.Field <$> nonEmpty fieldCharacter
+
+nonEmpty
+  :: Parsec.ParsecT s u m a -> Parsec.ParsecT s u m (NonEmpty.NonEmpty a)
+nonEmpty p = (NonEmpty.:|) <$> p <*> Parsec.many p
+
+fieldCharacter
+  :: Parsec.Stream s m Char
+  => Parsec.ParsecT s u m (Character.Character Field.Field)
+fieldCharacter = choice encodedCharacter (unencodedCharacter isFieldCharacter)
+
+encodedCharacter
+  :: Parsec.Stream s m Char => Parsec.ParsecT s u m (Character.Character tag)
+encodedCharacter = Parsec.char '%' >> Character.Encoded <$> digit <*> digit
+
+digit :: Parsec.Stream s m Char => Parsec.ParsecT s u m Digit.Digit
+digit = do
+  x <- Parsec.satisfy Char.isHexDigit
+  maybe (fail "invalid Digit") pure $ Digit.fromChar x
+
+unencodedCharacter
+  :: Parsec.Stream s m Char
+  => (Char -> Bool)
+  -> Parsec.ParsecT s u m (Character.Character tag)
+unencodedCharacter = fmap Character.Unencoded . Parsec.satisfy
+
+isFieldCharacter :: Char -> Bool
+isFieldCharacter x = case x of
+  '_' -> True
+  _ -> Char.isAsciiUpper x || Char.isAsciiLower x || Char.isDigit x
+
+modifier :: Parsec.Stream s m Char => Parsec.ParsecT s u m Modifier.Modifier
+modifier = Parsec.option Modifier.None $ Parsec.choice
+  [ Modifier.Asterisk <$ Parsec.char '*'
+  , Parsec.char ':' >> Modifier.Colon <$> maxLength
+  ]
+
+maxLength :: Parsec.Stream s m Char => Parsec.ParsecT s u m MaxLength.MaxLength
+maxLength = do
+  x <- Parsec.satisfy $ Ix.inRange ('1', '9')
+  xs <- Parsec.many $ Parsec.satisfy Char.isDigit
+  n <- maybe (fail "invalid MaxLength") pure . Read.readMaybe $ x : xs
+  if isMaxLength n
+    then pure $ MaxLength.MaxLength n
+    else fail "invalid MaxLength"
+
+isMaxLength :: Int -> Bool
+isMaxLength = Ix.inRange (1, 9999)
+
+literal :: Parsec.Stream s m Char => Parsec.ParsecT s u m Literal.Literal
+literal = Literal.Literal <$> nonEmpty literalCharacter
+
+literalCharacter
+  :: Parsec.Stream s m Char
+  => Parsec.ParsecT s u m (Character.Character Literal.Literal)
+literalCharacter =
+  choice encodedCharacter (unencodedCharacter isLiteralCharacter)
+
+isLiteralCharacter :: Char -> Bool
+isLiteralCharacter x = case x of
+  ' ' -> False
+  '"' -> False
+  '\'' -> False
+  '%' -> False
+  '<' -> False
+  '>' -> False
+  '\\' -> False
+  '^' -> False
+  '`' -> False
+  '{' -> False
+  '|' -> False
+  '}' -> False
+  _ ->
+    Ix.inRange ('\x20', '\x7e') x
+      || Ix.inRange ('\xa0', '\xd7ff') x
+      || Ix.inRange ('\xe000', '\xf8ff') x
+      || Ix.inRange ('\xf900', '\xfdcf') x
+      || Ix.inRange ('\xfdf0', '\xffef') x
+      || Ix.inRange ('\x10000', '\x1fffd') x
+      || Ix.inRange ('\x20000', '\x2fffd') x
+      || Ix.inRange ('\x30000', '\x3fffd') x
+      || Ix.inRange ('\x40000', '\x4fffd') x
+      || Ix.inRange ('\x50000', '\x5fffd') x
+      || Ix.inRange ('\x60000', '\x6fffd') x
+      || Ix.inRange ('\x70000', '\x7fffd') x
+      || Ix.inRange ('\x80000', '\x8fffd') x
+      || Ix.inRange ('\x90000', '\x9fffd') x
+      || Ix.inRange ('\xa0000', '\xafffd') x
+      || Ix.inRange ('\xb0000', '\xbfffd') x
+      || Ix.inRange ('\xc0000', '\xcfffd') x
+      || Ix.inRange ('\xd0000', '\xdfffd') x
+      || Ix.inRange ('\xe1000', '\xefffd') x
+      || Ix.inRange ('\xf0000', '\xffffd') x
+      || Ix.inRange ('\x100000', '\x10fffd') x
diff --git a/src/lib/Burrito/Internal/Render.hs b/src/lib/Burrito/Internal/Render.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Render.hs
@@ -0,0 +1,121 @@
+module Burrito.Internal.Render where
+
+import qualified Burrito.Internal.Type.Case as Case
+import qualified Burrito.Internal.Type.Character as Character
+import qualified Burrito.Internal.Type.Digit as Digit
+import qualified Burrito.Internal.Type.Expression as Expression
+import qualified Burrito.Internal.Type.Field as Field
+import qualified Burrito.Internal.Type.Literal as Literal
+import qualified Burrito.Internal.Type.MaxLength as MaxLength
+import qualified Burrito.Internal.Type.Modifier as Modifier
+import qualified Burrito.Internal.Type.Name as Name
+import qualified Burrito.Internal.Type.Operator as Operator
+import qualified Burrito.Internal.Type.Template as Template
+import qualified Burrito.Internal.Type.Token as Token
+import qualified Burrito.Internal.Type.Variable as Variable
+import qualified Data.List as List
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Text.Lazy as LazyText
+import qualified Data.Text.Lazy.Builder as Builder
+
+-- | Renders a template back into a string. This is essentially the opposite of
+-- @parse@. Usually you'll want to use @expand@ to actually substitute
+-- variables in the template, but this can be useful for printing out the
+-- template itself
+--
+-- >>> render <$> parse "valid-template"
+-- Just "valid-template"
+-- >>> render <$> parse "{var}"
+-- Just "{var}"
+render :: Template.Template -> String
+render = builderToString . template
+
+builderToString :: Builder.Builder -> String
+builderToString = LazyText.unpack . Builder.toLazyText
+
+template :: Template.Template -> Builder.Builder
+template = foldMap token . Template.tokens
+
+token :: Token.Token -> Builder.Builder
+token x = case x of
+  Token.Expression y -> expression y
+  Token.Literal y -> literal y
+
+expression :: Expression.Expression -> Builder.Builder
+expression x =
+  Builder.singleton '{'
+    <> operator (Expression.operator x)
+    <> sepBy1 variable (Builder.singleton ',') (Expression.variables x)
+    <> Builder.singleton '}'
+
+operator :: Operator.Operator -> Builder.Builder
+operator x = case x of
+  Operator.Ampersand -> Builder.singleton '&'
+  Operator.FullStop -> Builder.singleton '.'
+  Operator.None -> mempty
+  Operator.NumberSign -> Builder.singleton '#'
+  Operator.PlusSign -> Builder.singleton '+'
+  Operator.QuestionMark -> Builder.singleton '?'
+  Operator.Semicolon -> Builder.singleton ';'
+  Operator.Solidus -> Builder.singleton '/'
+
+sepBy1
+  :: (a -> Builder.Builder)
+  -> Builder.Builder
+  -> NonEmpty.NonEmpty a
+  -> Builder.Builder
+sepBy1 f x = mconcat . List.intersperse x . fmap f . NonEmpty.toList
+
+variable :: Variable.Variable -> Builder.Builder
+variable x = name (Variable.name x) <> modifier (Variable.modifier x)
+
+name :: Name.Name -> Builder.Builder
+name = sepBy1 field (Builder.singleton '.') . Name.fields
+
+field :: Field.Field -> Builder.Builder
+field = foldMap character . Field.characters
+
+character :: Character.Character tag -> Builder.Builder
+character x = case x of
+  Character.Encoded y z -> encodedCharacter y z
+  Character.Unencoded y -> Builder.singleton y
+
+encodedCharacter :: Digit.Digit -> Digit.Digit -> Builder.Builder
+encodedCharacter x y = Builder.singleton '%' <> digit x <> digit y
+
+digit :: Digit.Digit -> Builder.Builder
+digit x = Builder.singleton $ case x of
+  Digit.Ox0 -> '0'
+  Digit.Ox1 -> '1'
+  Digit.Ox2 -> '2'
+  Digit.Ox3 -> '3'
+  Digit.Ox4 -> '4'
+  Digit.Ox5 -> '5'
+  Digit.Ox6 -> '6'
+  Digit.Ox7 -> '7'
+  Digit.Ox8 -> '8'
+  Digit.Ox9 -> '9'
+  Digit.OxA Case.Upper -> 'A'
+  Digit.OxB Case.Upper -> 'B'
+  Digit.OxC Case.Upper -> 'C'
+  Digit.OxD Case.Upper -> 'D'
+  Digit.OxE Case.Upper -> 'E'
+  Digit.OxF Case.Upper -> 'F'
+  Digit.OxA Case.Lower -> 'a'
+  Digit.OxB Case.Lower -> 'b'
+  Digit.OxC Case.Lower -> 'c'
+  Digit.OxD Case.Lower -> 'd'
+  Digit.OxE Case.Lower -> 'e'
+  Digit.OxF Case.Lower -> 'f'
+
+modifier :: Modifier.Modifier -> Builder.Builder
+modifier x = case x of
+  Modifier.Asterisk -> Builder.singleton '*'
+  Modifier.Colon y -> Builder.singleton ':' <> maxLength y
+  Modifier.None -> mempty
+
+maxLength :: MaxLength.MaxLength -> Builder.Builder
+maxLength = Builder.fromString . show . MaxLength.count
+
+literal :: Literal.Literal -> Builder.Builder
+literal = foldMap character . Literal.characters
diff --git a/src/lib/Burrito/Internal/TH.hs b/src/lib/Burrito/Internal/TH.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/TH.hs
@@ -0,0 +1,28 @@
+module Burrito.Internal.TH where
+
+import qualified Burrito.Internal.Parse as Parse
+import qualified Language.Haskell.TH.Quote as TH
+import qualified Language.Haskell.TH.Syntax as TH
+
+-- | This can be used together with the @QuasiQuotes@ language extension to
+-- parse a URI template at compile time. This is convenient because it allows
+-- you to verify the validity of the template when you compile your file as
+-- opposed to when you run it.
+--
+-- >>> :set -XQuasiQuotes
+-- >>> import Burrito
+-- >>> let template = [uriTemplate|http://example/search{?query}|]
+-- >>> let values = [("query", stringValue "chorizo")]
+-- >>> expand values template
+-- "http://example/search?query=chorizo"
+--
+-- Note that you cannot use escape sequences in this quasi-quoter. For example,
+-- this is invalid: @[uriTemplate|\\xa0|]@. You can however use percent encoded
+-- triples as normal. So this is valid: @[uriTemplate|%c2%a0|]@.
+uriTemplate :: TH.QuasiQuoter
+uriTemplate = TH.QuasiQuoter
+  { TH.quoteDec = const $ fail "cannot be used as a declaration"
+  , TH.quoteExp = maybe (fail "invalid URI template") TH.liftData . Parse.parse
+  , TH.quotePat = const $ fail "cannot be used as a pattern"
+  , TH.quoteType = const $ fail "cannot be used as a type"
+  }
diff --git a/src/lib/Burrito/Internal/Type/Case.hs b/src/lib/Burrito/Internal/Type/Case.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Case.hs
@@ -0,0 +1,10 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Case where
+
+import qualified Data.Data as Data
+
+data Case
+  = Lower
+  | Upper
+  deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Character.hs b/src/lib/Burrito/Internal/Type/Character.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Character.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Character where
+
+import qualified Burrito.Internal.Type.Digit as Digit
+import qualified Data.Data as Data
+
+data Character tag
+  = Encoded Digit.Digit Digit.Digit
+  | Unencoded Char
+  deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Digit.hs b/src/lib/Burrito/Internal/Type/Digit.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Digit.hs
@@ -0,0 +1,98 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Digit where
+
+import qualified Burrito.Internal.Type.Case as Case
+import qualified Data.Bits as Bits
+import qualified Data.Word as Word
+import qualified Data.Data as Data
+
+data Digit
+  = Ox0
+  | Ox1
+  | Ox2
+  | Ox3
+  | Ox4
+  | Ox5
+  | Ox6
+  | Ox7
+  | Ox8
+  | Ox9
+  | OxA Case.Case
+  | OxB Case.Case
+  | OxC Case.Case
+  | OxD Case.Case
+  | OxE Case.Case
+  | OxF Case.Case
+  deriving (Data.Data, Eq, Ord, Show)
+
+fromChar :: Char -> Maybe Digit
+fromChar x = case x of
+  '0' -> Just Ox0
+  '1' -> Just Ox1
+  '2' -> Just Ox2
+  '3' -> Just Ox3
+  '4' -> Just Ox4
+  '5' -> Just Ox5
+  '6' -> Just Ox6
+  '7' -> Just Ox7
+  '8' -> Just Ox8
+  '9' -> Just Ox9
+  'A' -> Just $ OxA Case.Upper
+  'B' -> Just $ OxB Case.Upper
+  'C' -> Just $ OxC Case.Upper
+  'D' -> Just $ OxD Case.Upper
+  'E' -> Just $ OxE Case.Upper
+  'F' -> Just $ OxF Case.Upper
+  'a' -> Just $ OxA Case.Lower
+  'b' -> Just $ OxB Case.Lower
+  'c' -> Just $ OxC Case.Lower
+  'd' -> Just $ OxD Case.Lower
+  'e' -> Just $ OxE Case.Lower
+  'f' -> Just $ OxF Case.Lower
+  _ -> Nothing
+
+fromWord8 :: Word.Word8 -> (Digit, Digit)
+fromWord8 x =
+  let
+    f y = case y of
+      0x0 -> Ox0
+      0x1 -> Ox1
+      0x2 -> Ox2
+      0x3 -> Ox3
+      0x4 -> Ox4
+      0x5 -> Ox5
+      0x6 -> Ox6
+      0x7 -> Ox7
+      0x8 -> Ox8
+      0x9 -> Ox9
+      0xA -> OxA Case.Upper
+      0xB -> OxB Case.Upper
+      0xC -> OxC Case.Upper
+      0xD -> OxD Case.Upper
+      0xE -> OxE Case.Upper
+      0xF -> OxF Case.Upper
+      _ -> error $ "invalid nibble: " <> show y
+  in (f $ Bits.shiftR x 4, f $ x Bits..&. 0x0F)
+
+toWord8 :: Digit -> Digit -> Word.Word8
+toWord8 x y =
+  let
+    f z = case z of
+      Ox0 -> 0x0
+      Ox1 -> 0x1
+      Ox2 -> 0x2
+      Ox3 -> 0x3
+      Ox4 -> 0x4
+      Ox5 -> 0x5
+      Ox6 -> 0x6
+      Ox7 -> 0x7
+      Ox8 -> 0x8
+      Ox9 -> 0x9
+      OxA _ -> 0xA
+      OxB _ -> 0xB
+      OxC _ -> 0xC
+      OxD _ -> 0xD
+      OxE _ -> 0xE
+      OxF _ -> 0xF
+  in Bits.shiftL (f x) 4 Bits..|. f y
diff --git a/src/lib/Burrito/Internal/Type/Expression.hs b/src/lib/Burrito/Internal/Type/Expression.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Expression.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Expression where
+
+import qualified Burrito.Internal.Type.Operator as Operator
+import qualified Burrito.Internal.Type.Variable as Variable
+import qualified Data.Data as Data
+import qualified Data.List.NonEmpty as NonEmpty
+
+data Expression = Expression
+  { operator :: Operator.Operator
+  , variables :: NonEmpty.NonEmpty Variable.Variable
+  } deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Field.hs b/src/lib/Burrito/Internal/Type/Field.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Field.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Field where
+
+import qualified Burrito.Internal.Type.Character as Character
+import qualified Data.Data as Data
+import qualified Data.List.NonEmpty as NonEmpty
+
+newtype Field = Field
+  { characters :: NonEmpty.NonEmpty (Character.Character Field)
+  } deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Literal.hs b/src/lib/Burrito/Internal/Type/Literal.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Literal.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Literal where
+
+import qualified Burrito.Internal.Type.Character as Character
+import qualified Data.Data as Data
+import qualified Data.List.NonEmpty as NonEmpty
+
+newtype Literal = Literal
+  { characters :: NonEmpty.NonEmpty (Character.Character Literal)
+  } deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Match.hs b/src/lib/Burrito/Internal/Type/Match.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Match.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Match where
+
+import qualified Burrito.Internal.Type.MaxLength as MaxLength
+import qualified Data.Data as Data
+import qualified Data.Text as Text
+
+data Match
+  = Defined Text.Text
+  | Prefix MaxLength.MaxLength Text.Text
+  | Undefined
+  deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/MaxLength.hs b/src/lib/Burrito/Internal/Type/MaxLength.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/MaxLength.hs
@@ -0,0 +1,9 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.MaxLength where
+
+import qualified Data.Data as Data
+
+newtype MaxLength = MaxLength
+  { count :: Int
+  } deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Modifier.hs b/src/lib/Burrito/Internal/Type/Modifier.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Modifier.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Modifier where
+
+import qualified Burrito.Internal.Type.MaxLength as MaxLength
+import qualified Data.Data as Data
+
+data Modifier
+  = Asterisk
+  | Colon MaxLength.MaxLength
+  | None
+  deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Name.hs b/src/lib/Burrito/Internal/Type/Name.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Name.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Name where
+
+import qualified Burrito.Internal.Type.Field as Field
+import qualified Data.Data as Data
+import qualified Data.List.NonEmpty as NonEmpty
+
+newtype Name = Name
+  { fields :: NonEmpty.NonEmpty Field.Field
+  } deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Operator.hs b/src/lib/Burrito/Internal/Type/Operator.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Operator.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Operator where
+
+import qualified Data.Data as Data
+
+data Operator
+  = Ampersand
+  | FullStop
+  | None
+  | NumberSign
+  | PlusSign
+  | QuestionMark
+  | Semicolon
+  | Solidus
+  deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Template.hs b/src/lib/Burrito/Internal/Type/Template.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Template.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Template where
+
+import qualified Burrito.Internal.Type.Token as Token
+import qualified Data.Data as Data
+
+-- | Represents a URI template.
+newtype Template = Template
+  { tokens :: [Token.Token]
+  } deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Token.hs b/src/lib/Burrito/Internal/Type/Token.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Token.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Token where
+
+import qualified Burrito.Internal.Type.Expression as Expression
+import qualified Burrito.Internal.Type.Literal as Literal
+import qualified Data.Data as Data
+
+data Token
+  = Expression Expression.Expression
+  | Literal Literal.Literal
+  deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Value.hs b/src/lib/Burrito/Internal/Type/Value.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Value.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Value where
+
+import qualified Data.Data as Data
+import qualified Data.Map as Map
+import qualified Data.Text as Text
+
+-- | Represents a value that can be substituted into a template. Can be a
+-- string, a list, or dictionary (which is called an associative array in the
+-- RFC).
+data Value
+  = Dictionary (Map.Map Text.Text Text.Text)
+  | List [Text.Text]
+  | String Text.Text
+  deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Internal/Type/Variable.hs b/src/lib/Burrito/Internal/Type/Variable.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Burrito/Internal/Type/Variable.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Burrito.Internal.Type.Variable where
+
+import qualified Burrito.Internal.Type.Modifier as Modifier
+import qualified Burrito.Internal.Type.Name as Name
+import qualified Data.Data as Data
+
+data Variable = Variable
+  { name :: Name.Name
+  , modifier :: Modifier.Modifier
+  } deriving (Data.Data, Eq, Ord, Show)
diff --git a/src/lib/Burrito/Parse.hs b/src/lib/Burrito/Parse.hs
deleted file mode 100644
--- a/src/lib/Burrito/Parse.hs
+++ /dev/null
@@ -1,403 +0,0 @@
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Parse
-  ( parse
-  )
-where
-
-import qualified Burrito.Type.Expression as Expression
-import qualified Burrito.Type.LitChar as LitChar
-import qualified Burrito.Type.Literal as Literal
-import qualified Burrito.Type.Modifier as Modifier
-import qualified Burrito.Type.Name as Name
-import qualified Burrito.Type.NonEmpty as NonEmpty
-import qualified Burrito.Type.Operator as Operator
-import qualified Burrito.Type.Template as Template
-import qualified Burrito.Type.Token as Token
-import qualified Burrito.Type.VarChar as VarChar
-import qualified Burrito.Type.Variable as Variable
-import qualified Control.Applicative as Applicative
-import qualified Control.Monad as Monad
-import qualified Data.Char as Char
-import qualified Data.Maybe as Maybe
-import qualified Data.Word as Word
-
-
--- | Attempts to parse a string as a URI template. If parsing fails, this will
--- return 'Nothing'. Otherwise it will return 'Just' the parsed template.
---
--- Parsing will usually succeed, but it can fail if the input string contains
--- characters that are not valid in IRIs (like @^@) or if the input string
--- contains an invalid template expression (like @{!}@). To include characters
--- that aren't valid in IRIs, percent encode them (like @%5E@).
-parse :: String -> Maybe Template.Template
-parse string = case runParser parseTemplate string of
-  Just (template, "") -> Just template
-  _ -> Nothing
-
-
--- | Converts a machine-sized signed integer into an eight-bit unsigned
--- integer. If the input is out of bounds, an exception will be thrown.
-intToWord8 :: Int -> Word.Word8
-intToWord8 x =
-  let
-    lo = word8ToInt (minBound :: Word.Word8)
-    hi = word8ToInt (maxBound :: Word.Word8)
-  in if x < lo
-    then error $ "intToWord8: " <> show x <> " < " <> show lo
-    else if x > hi
-      then error $ "intToWord8: " <> show x <> " > " <> show hi
-      else fromIntegral x
-
-
--- | Converts an eight-bit unsigned integer into a machine-sized signed
--- integer. This conversion cannot fail.
-word8ToInt :: Word.Word8 -> Int
-word8ToInt = fromIntegral
-
-
--- | A simple type to handle parsing.
-newtype Parser a = Parser
-  { runParser :: String -> Maybe (a, String)
-  }
-
-
-instance Functor Parser where
-  -- | Applies the given function to the result of a successful parse.
-  fmap f p = Parser $ \s -> case runParser p s of
-    Nothing -> Nothing
-    Just (x, t) -> Just (f x, t)
-
-
-instance Applicative Parser where
-  -- | Produces a parser that always succeeds by returning the given value.
-  pure x = Parser $ \s -> Just (x, s)
-
-  -- | Uses the first parser to get a function, then uses the second parser to
-  -- get a value, then calls the function with the value.
-  p <*> q = Parser $ \s -> case runParser p s of
-    Nothing -> Nothing
-    Just (f, t) -> case runParser q t of
-      Nothing -> Nothing
-      Just (x, u) -> Just (f x, u)
-
-
-instance Monad Parser where
-  -- | Feeds the output of a successful parse into the given function. If
-  -- parsing fails, doesn't call the function.
-  p >>= f = Parser $ \s -> case runParser p s of
-    Nothing -> Nothing
-    Just (x, t) -> runParser (f x) t
-
-
-instance Applicative.Alternative Parser where
-  -- | Fails without consuming any input.
-  empty = Parser $ const Nothing
-
-  -- | Returns the first parser if it succeeds. Otherwise returns the second
-  -- parser.
-  p <|> q = Parser $ \s -> case runParser p s of
-    Nothing -> runParser q s
-    Just (x, t) -> Just (x, t)
-
-
--- | Parses any one character. This is used as the basis for all other parsers.
-parseAny :: Parser Char
-parseAny = Parser $ \string -> case string of
-  "" -> Nothing
-  first : rest -> Just (first, rest)
-
-
--- | Runs the given parser between the other parsers. Useful for wrapping a
--- parser in quotes or parentheses.
-parseBetween :: Parser before -> Parser after -> Parser a -> Parser a
-parseBetween before after parser = before *> parser <* after
-
-
--- | Parses the given character and returns it.
-parseChar :: Char -> Parser Char
-parseChar = parseIf . (==)
-
-
--- | Parses the given character and throws it away. See 'parseChar'.
-parseChar_ :: Char -> Parser ()
-parseChar_ = Monad.void . parseChar
-
-
--- | Tries to parse the first thing. If that fails, tries to parse the second
--- thing.
-parseEither :: Parser a -> Parser a -> Parser a
-parseEither = (Applicative.<|>)
-
-
--- | Parses one character if it passes the given predicate function.
-parseIf :: (Char -> Bool) -> Parser Char
-parseIf predicate = do
-  char <- parseAny
-  if predicate char then pure char else Applicative.empty
-
-
--- | Runs the given parser at least once.
-parseNonEmpty :: Parser a -> Parser (NonEmpty.NonEmpty a)
-parseNonEmpty parser = nonEmpty <$> parser <*> Applicative.many parser
-
-
--- | Runs the given parser separated by the other parser. Requires at least one
--- occurrence of the non-separator parser.
-parseSepBy1 :: Parser separator -> Parser a -> Parser (NonEmpty.NonEmpty a)
-parseSepBy1 separator parser =
-  nonEmpty <$> parser <*> Applicative.many (separator *> parser)
-
-
--- | Parses a @URI-Template@ as defined by section 2 of the RFC.
-parseTemplate :: Parser Template.Template
-parseTemplate = Template.Template <$> Applicative.many parseToken
-
-
--- | Parses a token, which we define as part of a URI template.
-parseToken :: Parser Token.Token
-parseToken = parseEither
-  (Token.Literal <$> parseLiteral)
-  (Token.Expression <$> parseExpression)
-
-
--- | Parses a @literals@ value as defined by section 2.1 of the RFC.
-parseLiteral :: Parser Literal.Literal
-parseLiteral = Literal.Literal <$> parseNonEmpty parseCharacter
-
-
--- | Parses a character in a literal.
-parseCharacter :: Parser LitChar.LitChar
-parseCharacter = parseEither parseCharacterUnencoded parseCharacterEncoded
-
-
--- | Parses an unencoded character in a literal.
-parseCharacterUnencoded :: Parser LitChar.LitChar
-parseCharacterUnencoded = do
-  char <- parseIf LitChar.isLiteral
-  maybe Applicative.empty pure $ LitChar.makeUnencoded char
-
-
--- | Parses a percent-encoded character in a literal.
-parseCharacterEncoded :: Parser LitChar.LitChar
-parseCharacterEncoded = do
-  (hi, lo) <- parsePercentEncoded
-  pure . LitChar.Encoded $ intToWord8
-    (Char.digitToInt hi * 16 + Char.digitToInt lo)
-
-
--- | Parses an @expression@ as defined by section 2.2 of the RFC.
-parseExpression :: Parser Expression.Expression
-parseExpression =
-  parseBetween (parseChar_ '{') (parseChar_ '}')
-    $ Expression.Expression
-    <$> parseOperator
-    <*> parseVariableList
-
-
--- | Parses a @variable-list@ as defined by sections 2.3 of the RFC.
-parseVariableList :: Parser (NonEmpty.NonEmpty Variable.Variable)
-parseVariableList = parseSepBy1 (parseChar_ ',') parseVarspec
-
-
--- | Parses a @varspec@ as defined by section 2.3 of the RFC.
-parseVarspec :: Parser Variable.Variable
-parseVarspec = do
-  name <- parseVarname
-  modifier <- parseModifier
-  pure $ Variable.Variable
-    { Variable.name = name
-    , Variable.modifier = modifier
-    }
-
-
--- | Parses a @varname@ as defined by section 2.3 of the RFC.
-parseVarname :: Parser Name.Name
-parseVarname = do
-  first <- parseVarcharFirst
-  rest <- Applicative.many parseVarcharRest
-  pure Name.Name { Name.first = first, Name.rest = rest }
-
-
--- | Parses the first character in a variable name, which excludes periods.
-parseVarcharFirst :: Parser VarChar.VarChar
-parseVarcharFirst = parseEither parseVarcharUnencoded parseVarcharEncoded
-
-
--- | Parses an unencoded character in a variable name.
-parseVarcharUnencoded :: Parser VarChar.VarChar
-parseVarcharUnencoded = do
-  char <- parseIf VarChar.isVarchar
-  maybe Applicative.empty pure $ VarChar.makeUnencoded char
-
-
--- | Parses a percent-encoded character in a variable name.
-parseVarcharEncoded :: Parser VarChar.VarChar
-parseVarcharEncoded = do
-  (hi, lo) <- parsePercentEncoded
-  maybe Applicative.empty pure $ VarChar.makeEncoded hi lo
-
-
--- | Parses a non-first character in a variable name. This is like
--- 'parseVarcharFirst' except it allows periods.
-parseVarcharRest :: Parser (Bool, VarChar.VarChar)
-parseVarcharRest =
-  (,)
-    <$> parseEither (True <$ parseChar_ '.') (pure False)
-    <*> parseVarcharFirst
-
-
--- | Constructs a non-empty list without using an operator.
-nonEmpty :: a -> [a] -> NonEmpty.NonEmpty a
-nonEmpty = NonEmpty.NonEmpty
-
-
--- | Parses a @pct-encoded@ as defined by section 1.5 of the RFC. Returns both
--- hexadecimal digits as they appeared in the input without doing any case
--- normalization.
-parsePercentEncoded :: Parser (Char, Char)
-parsePercentEncoded = do
-  parseChar_ '%'
-  (,) <$> parseIf Char.isHexDigit <*> parseIf Char.isHexDigit
-
-
--- | Parses an @operator@ as defined by section 2.2 of the RFC.
-parseOperator :: Parser Operator.Operator
-parseOperator =
-  Maybe.fromMaybe Operator.None <$> Applicative.optional parseRequiredOperator
-
-
--- | Parses a required, non-reserved operator as defined by section 2.2 of the
--- RFC. See 'parseOperator'.
-parseRequiredOperator :: Parser Operator.Operator
-parseRequiredOperator = do
-  operator <- parseIf isOperator
-  maybe Applicative.empty pure $ toOperator operator
-
-
--- | Converts an operator character into its respective operator type. Returns
--- nothing for characters that are not valid operators.
-toOperator :: Char -> Maybe Operator.Operator
-toOperator x = case x of
-  '+' -> Just Operator.PlusSign
-  '#' -> Just Operator.NumberSign
-  '.' -> Just Operator.FullStop
-  '/' -> Just Operator.Solidus
-  ';' -> Just Operator.Semicolon
-  '?' -> Just Operator.QuestionMark
-  '&' -> Just Operator.Ampersand
-  _ -> Nothing
-
-
--- | Returns true if the given character is in the @operator@ range defined by
--- section 2.2 of the RFC.
-isOperator :: Char -> Bool
-isOperator x = isOpLevel2 x || isOpLevel3 x || isOpReserve x
-
-
--- | Returns true if the given character is in the @op-level2@ range defined by
--- section 2.2 of the RFC.
-isOpLevel2 :: Char -> Bool
-isOpLevel2 x = case x of
-  '+' -> True
-  '#' -> True
-  _ -> False
-
-
--- | Returns true if the given character is in the @op-level3@ range defined by
--- section 2.2 of the RFC.
-isOpLevel3 :: Char -> Bool
-isOpLevel3 x = case x of
-  '.' -> True
-  '/' -> True
-  ';' -> True
-  '?' -> True
-  '&' -> True
-  _ -> False
-
-
--- | Returns true if the given character is in the @op-reserve@ range defined
--- by section 2.2 of the RFC.
-isOpReserve :: Char -> Bool
-isOpReserve x = case x of
-  '=' -> True
-  ',' -> True
-  '!' -> True
-  '@' -> True
-  '|' -> True
-  _ -> False
-
-
--- | Parses a @modifier-level4@ as defined by section 2.4 of the RFC.
-parseModifier :: Parser Modifier.Modifier
-parseModifier =
-  fmap (Maybe.fromMaybe Modifier.None) . Applicative.optional $ parseEither
-    parsePrefixModifier
-    parseExplodeModifier
-
-
--- | Parses a @prefix@ as defined by section 2.4.1 of the RFC.
-parsePrefixModifier :: Parser Modifier.Modifier
-parsePrefixModifier = do
-  parseChar_ ':'
-  maxLength <- parseMaxLength
-  maybe Applicative.empty pure $ Modifier.makeColon maxLength
-
-
--- | Parses a @max-length@ as defined by section 2.4.1 of the RFC.
-parseMaxLength :: Parser Int
-parseMaxLength = do
-  first <- parseNonZeroDigit
-  rest <- parseUpTo 3 parseDigit
-  pure . fromDigits $ rest <> [first]
-
-
--- | Converts a backwards list of digits into the number that they represent.
--- For example @[2, 1]@ becomes @12@.
-fromDigits :: [Int] -> Int
-fromDigits = foldr (\digit -> (+ digit) . (* 10)) 0
-
-
--- | Parses up to the given number of occurrences of the given parser. If the
--- number is less than one, this will always succeed by returning the empty
--- list.
---
--- Note that for performance reasons this returns the list in reverse order. If
--- you need it in the order it was present in the input, use @reverse@.
-parseUpTo :: Int -> Parser a -> Parser [a]
-parseUpTo = parseUpToWith []
-
-
--- | Like 'parseUpTo' but with an explicit accumulator.
-parseUpToWith :: [a] -> Int -> Parser a -> Parser [a]
-parseUpToWith accumulator remaining parser = if remaining < 1
-  then pure accumulator
-  else do
-    result <- Applicative.optional parser
-    case result of
-      Nothing -> pure accumulator
-      Just value -> parseUpToWith (value : accumulator) (remaining - 1) parser
-
-
--- | Parses a single non-zero decimal digit and returns that digit's value. See
--- 'isNonZeroDigit'.
-parseNonZeroDigit :: Parser Int
-parseNonZeroDigit = Char.digitToInt <$> parseIf isNonZeroDigit
-
-
--- | Returns true if the given character is a non-zero decimal digit. This
--- range isn't explicitly named by the RFC, but it's given in section 2.4.1.
-isNonZeroDigit :: Char -> Bool
-isNonZeroDigit x = case x of
-  '0' -> False
-  _ -> Char.isDigit x
-
-
--- | Parses a single decimal digit and returns that digit's value.
-parseDigit :: Parser Int
-parseDigit = Char.digitToInt <$> parseIf Char.isDigit
-
-
--- | Parses an @explode@ as defined by section 2.4.2 of the RFC.
-parseExplodeModifier :: Parser Modifier.Modifier
-parseExplodeModifier = Modifier.Asterisk <$ parseChar_ '*'
diff --git a/src/lib/Burrito/Render.hs b/src/lib/Burrito/Render.hs
deleted file mode 100644
--- a/src/lib/Burrito/Render.hs
+++ /dev/null
@@ -1,121 +0,0 @@
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Render
-  ( render
-  )
-where
-
-import qualified Burrito.Type.Expression as Expression
-import qualified Burrito.Type.LitChar as LitChar
-import qualified Burrito.Type.Literal as Literal
-import qualified Burrito.Type.Modifier as Modifier
-import qualified Burrito.Type.Name as Name
-import qualified Burrito.Type.NonEmpty as NonEmpty
-import qualified Burrito.Type.Operator as Operator
-import qualified Burrito.Type.Template as Template
-import qualified Burrito.Type.Token as Token
-import qualified Burrito.Type.VarChar as VarChar
-import qualified Burrito.Type.Variable as Variable
-import qualified Data.List as List
-import qualified Data.Word as Word
-import qualified Text.Printf as Printf
-
-
--- | Renders a template back into a string. This is essentially the opposite of
--- @parse@.
-render :: Template.Template -> String
-render = concatMap renderToken . Template.tokens
-
-
--- | Renders a token in a template.
-renderToken :: Token.Token -> String
-renderToken token = case token of
-  Token.Expression expression -> renderExpression expression
-  Token.Literal literal -> renderLiteral literal
-
-
--- | Renders an expression token.
-renderExpression :: Expression.Expression -> String
-renderExpression expression = mconcat
-  [ "{"
-  , renderOperator $ Expression.operator expression
-  , renderVariables $ Expression.variables expression
-  , "}"
-  ]
-
-
--- | Renders an operator in an expression.
-renderOperator :: Operator.Operator -> String
-renderOperator operator = case operator of
-  Operator.Ampersand -> "&"
-  Operator.FullStop -> "."
-  Operator.None -> ""
-  Operator.NumberSign -> "#"
-  Operator.PlusSign -> "+"
-  Operator.QuestionMark -> "?"
-  Operator.Semicolon -> ";"
-  Operator.Solidus -> "/"
-
-
--- | Renders a bunch of variables in an expression.
-renderVariables :: NonEmpty.NonEmpty Variable.Variable -> String
-renderVariables = List.intercalate "," . fmap renderVariable . NonEmpty.toList
-
-
--- | Renders a variable in an expression.
-renderVariable :: Variable.Variable -> String
-renderVariable variable = mconcat
-  [ renderName $ Variable.name variable
-  , renderModifier $ Variable.modifier variable
-  ]
-
-
--- | Renders a variable name.
-renderName :: Name.Name -> String
-renderName name = mconcat
-  [ renderVarChar $ Name.first name
-  , concatMap
-      (\(fullStop, varChar) ->
-        (if fullStop then "." else "") <> renderVarChar varChar
-      )
-    $ Name.rest name
-  ]
-
-
--- | Renders one logical character of a variable name.
-renderVarChar :: VarChar.VarChar -> String
-renderVarChar varChar = case varChar of
-  VarChar.Encoded hi lo -> ['%', hi, lo]
-  VarChar.Unencoded char -> [char]
-
-
--- | Renders a variable modifier.
-renderModifier :: Modifier.Modifier -> String
-renderModifier modifier = case modifier of
-  Modifier.Asterisk -> "*"
-  Modifier.Colon int -> Printf.printf ":%d" int
-  Modifier.None -> ""
-
-
--- | Renders a literal token.
-renderLiteral :: Literal.Literal -> String
-renderLiteral =
-  concatMap renderCharacter . NonEmpty.toList . Literal.characters
-
-
--- | Renders a character in a literal token.
-renderCharacter :: LitChar.LitChar -> String
-renderCharacter character = case character of
-  LitChar.Encoded word8 -> renderEncodedCharacter word8
-  LitChar.Unencoded char -> renderUnencodedCharacter char
-
-
--- | Renders an encoded character by percent encoding it with uppercase
--- hexadecimal digits.
-renderEncodedCharacter :: Word.Word8 -> String
-renderEncodedCharacter = Printf.printf "%%%02X"
-
-
--- | Renders an unencoded character by simply turning it into a string.
-renderUnencodedCharacter :: Char -> String
-renderUnencodedCharacter = pure
diff --git a/src/lib/Burrito/TH.hs b/src/lib/Burrito/TH.hs
deleted file mode 100644
--- a/src/lib/Burrito/TH.hs
+++ /dev/null
@@ -1,34 +0,0 @@
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.TH
-  ( uriTemplate
-  )
-where
-
-import qualified Burrito.Parse as Parse
-import qualified Language.Haskell.TH.Quote as TH
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | This can be used together with the @QuasiQuotes@ language extension to
--- parse a URI template at compile time. This is convenient because it allows
--- you to verify the validity of the template when you compile your file as
--- opposed to when you run it.
---
--- >>> :set -XQuasiQuotes
--- >>> import Burrito
--- >>> let template = [uriTemplate|http://example/search{?query}|]
--- >>> let values = [("query", stringValue "chorizo")]
--- >>> expand values template
--- "http://example/search?query=chorizo"
---
--- Note that you cannot use escape sequences in this quasi-quoter. For example,
--- this is invalid: @[uriTemplate|\\xa0|]@. You can however use percent encoded
--- triples as normal. So this is valid: @[uriTemplate|%c2%a0|]@.
-uriTemplate :: TH.QuasiQuoter
-uriTemplate = TH.QuasiQuoter
-  { TH.quoteDec = const $ fail "cannot be used as a declaration"
-  , TH.quoteExp = maybe (fail "invalid URI template") TH.lift . Parse.parse
-  , TH.quotePat = const $ fail "cannot be used as a pattern"
-  , TH.quoteType = const $ fail "cannot be used as a type"
-  }
diff --git a/src/lib/Burrito/Type/Expression.hs b/src/lib/Burrito/Type/Expression.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/Expression.hs
+++ /dev/null
@@ -1,20 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.Expression
-  ( Expression(..)
-  )
-where
-
-import qualified Burrito.Type.NonEmpty as NonEmpty
-import qualified Burrito.Type.Operator as Operator
-import qualified Burrito.Type.Variable as Variable
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | Represents an expression in a token.
-data Expression = Expression
-  { operator :: Operator.Operator
-  , variables :: NonEmpty.NonEmpty Variable.Variable
-  } deriving (Eq, TH.Lift, Show)
diff --git a/src/lib/Burrito/Type/LitChar.hs b/src/lib/Burrito/Type/LitChar.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/LitChar.hs
+++ /dev/null
@@ -1,93 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!
-module Burrito.Type.LitChar
-  ( LitChar(..)
-  , isLiteral
-  , makeUnencoded
-  )
-where
-
-import qualified Data.Word as Word
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | Represents a character in a literal. Although encoded characters are
--- allowed to have any value, typically they will not include most ASCII
--- printable characters. In other words @A@ is more likely than @%41@.
-data LitChar
-  = Encoded Word.Word8
-  -- ^ This deliberately is not case sensitive. The tokens @%aa@ and @%AA@ are
-  -- both represented as @Encoded 0xAA@.
-  | Unencoded Char
-  -- ^ This assumes that the character passes the @isLiteral@ predicate. You
-  -- should prefer using @makeUnencoded@ to create these values.
-  deriving (Eq, TH.Lift, Show)
-
-
--- | If the character passes @isLiteral@, returns an @Unencoded@ character.
--- Otherwise returns nothing.
-makeUnencoded :: Char -> Maybe LitChar
-makeUnencoded char = if isLiteral char then Just $ Unencoded char else Nothing
-
-
--- | Returns true if the given character is in the @literal@ range defined by
--- section 2.1 of the RFC.
-isLiteral :: Char -> Bool
-isLiteral x = case x of
-  ' ' -> False
-  '"' -> False
-  '\'' -> False
-  '%' -> False
-  '<' -> False
-  '>' -> False
-  '\\' -> False
-  '^' -> False
-  '`' -> False
-  '{' -> False
-  '|' -> False
-  '}' -> False
-  _ -> between '\x20' '\x7e' x || isUcschar x || isIprivate x
-
-
--- | Returns true if the given character is in the @ucschar@ range defined by
--- section 1.5 of the RFC.
-isUcschar :: Char -> Bool
-isUcschar x =
-  between '\xa0' '\xd7ff' x
-    || between '\xf900' '\xfdcf' x
-    || between '\xfdf0' '\xffef' x
-    || between '\x10000' '\x1fffd' x
-    || between '\x20000' '\x2fffd' x
-    || between '\x30000' '\x3fffd' x
-    || between '\x40000' '\x4fffd' x
-    || between '\x50000' '\x5fffd' x
-    || between '\x60000' '\x6fffd' x
-    || between '\x70000' '\x7fffd' x
-    || between '\x80000' '\x8fffd' x
-    || between '\x90000' '\x9fffd' x
-    || between '\xa0000' '\xafffd' x
-    || between '\xb0000' '\xbfffd' x
-    || between '\xc0000' '\xcfffd' x
-    || between '\xd0000' '\xdfffd' x
-    || between '\xe1000' '\xefffd' x
-
-
--- | Returns true if the given character is in the @iprivate@ range defined by
--- section 1.5 of the RFC.
-isIprivate :: Char -> Bool
-isIprivate x =
-  between '\xe000' '\xf8ff' x
-    || between '\xf0000' '\xffffd' x
-    || between '\x100000' '\x10fffd' x
-
-
--- | Returns true if the value is between the given inclusive bounds.
-between
-  :: Ord a
-  => a -- ^ lower bound
-  -> a -- ^ upper bound
-  -> a
-  -> Bool
-between lo hi x = lo <= x && x <= hi
diff --git a/src/lib/Burrito/Type/Literal.hs b/src/lib/Burrito/Type/Literal.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/Literal.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.Literal
-  ( Literal(..)
-  )
-where
-
-import qualified Burrito.Type.LitChar as LitChar
-import qualified Burrito.Type.NonEmpty as NonEmpty
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | Represents a literal in a token.
-newtype Literal = Literal
-  { characters :: NonEmpty.NonEmpty LitChar.LitChar
-  } deriving (Eq, TH.Lift, Show)
diff --git a/src/lib/Burrito/Type/Modifier.hs b/src/lib/Burrito/Type/Modifier.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/Modifier.hs
+++ /dev/null
@@ -1,34 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.Modifier
-  ( Modifier(..)
-  , makeColon
-  )
-where
-
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | Represents a modifier on a variable.
-data Modifier
-  = Asterisk
-  | Colon Int
-  -- ^ This assumes that the number passes the @isValidMaxLength@ predicate.
-  -- You should prefer using @makeColon@ to create these values.
-  | None
-  deriving (Eq, TH.Lift, Show)
-
-
--- | If the number passes @isValidMaxLength@, returns a @Colon@ modifier.
--- Otherwise returns nothing.
-makeColon :: Int -> Maybe Modifier
-makeColon maxLength =
-  if isValidMaxLength maxLength then Just $ Colon maxLength else Nothing
-
-
--- | Returns true if the given number is a valid @max-length@ as defined by
--- section 2.4.1 of the RFC.
-isValidMaxLength :: Int -> Bool
-isValidMaxLength maxLength = 1 <= maxLength && maxLength <= 9999
diff --git a/src/lib/Burrito/Type/Name.hs b/src/lib/Burrito/Type/Name.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/Name.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.Name
-  ( Name(..)
-  )
-where
-
-import qualified Burrito.Type.VarChar as VarChar
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | Represents a variable name, which is required to be non-empty. Variable
--- names allow ASCII letters and numbers, underscores, percent encoded triples,
--- and periods. However the periods cannot appear at the beginning or end, and
--- there can't be more than one of them in a row.
-data Name = Name
-  { first :: VarChar.VarChar
-  -- ^ The first character is any valid @varchar@, including underscores and
-  -- percent encoded triples.
-  , rest :: [(Bool, VarChar.VarChar)]
-  -- ^ Every other character has the same constraints, but they may also be
-  -- preceeded by a full stop (period). That's what the @Bool@ represents:
-  -- @True@ means there is a period.
-  } deriving (Eq, TH.Lift, Show)
diff --git a/src/lib/Burrito/Type/NonEmpty.hs b/src/lib/Burrito/Type/NonEmpty.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/NonEmpty.hs
+++ /dev/null
@@ -1,39 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.NonEmpty
-  ( NonEmpty(..)
-  , fromList
-  , singleton
-  , toList
-  )
-where
-
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | This simple type is used as a replacement for
--- @Data.List.NonEmpty.NonEmpty@ so that we can provide our own instances that
--- are consistent across versions of GHC.
-data NonEmpty a = NonEmpty
-  { first :: a
-  , rest :: [a]
-  } deriving (Eq, TH.Lift, Show)
-
-
--- | Attempts to convert a regular list into a non-empty list.
-fromList :: [a] -> Maybe (NonEmpty a)
-fromList xs = case xs of
-  [] -> Nothing
-  x : ys -> Just NonEmpty { first = x, rest = ys }
-
-
--- | Creates a non-empty list with a single element.
-singleton :: a -> NonEmpty a
-singleton x = NonEmpty { first = x, rest = [] }
-
-
--- | Converts a non-empty list back into a regular list.
-toList :: NonEmpty a -> [a]
-toList xs = first xs : rest xs
diff --git a/src/lib/Burrito/Type/Operator.hs b/src/lib/Burrito/Type/Operator.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/Operator.hs
+++ /dev/null
@@ -1,23 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.Operator
-  ( Operator(..)
-  )
-where
-
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | Represents an operator in an expression.
-data Operator
-  = Ampersand
-  | FullStop
-  | None
-  | NumberSign
-  | PlusSign
-  | QuestionMark
-  | Semicolon
-  | Solidus
-  deriving (Eq, TH.Lift, Show)
diff --git a/src/lib/Burrito/Type/Template.hs b/src/lib/Burrito/Type/Template.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/Template.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.Template
-  ( Template(..)
-  )
-where
-
-import qualified Burrito.Type.Token as Token
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | Represents a URI template.
-newtype Template = Template
-  { tokens :: [Token.Token]
-  } deriving (Eq, TH.Lift, Show)
diff --git a/src/lib/Burrito/Type/Token.hs b/src/lib/Burrito/Type/Token.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/Token.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.Token
-  ( Token(..)
-  )
-where
-
-import qualified Burrito.Type.Expression as Expression
-import qualified Burrito.Type.Literal as Literal
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | Represents a token in a template.
-data Token
-  = Expression Expression.Expression
-  | Literal Literal.Literal
-  deriving (Eq, TH.Lift, Show)
diff --git a/src/lib/Burrito/Type/Value.hs b/src/lib/Burrito/Type/Value.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/Value.hs
+++ /dev/null
@@ -1,16 +0,0 @@
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.Value
-  ( Value(..)
-  )
-where
-
-
--- | Represents a value that can be substituted into a template. Can be a
--- string, a list, or dictionary (which is called an associative array in the
--- RFC).
-data Value
-  = Dictionary [(String, String)]
-  | List [String]
-  | String String
-  deriving (Eq, Show)
diff --git a/src/lib/Burrito/Type/VarChar.hs b/src/lib/Burrito/Type/VarChar.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/VarChar.hs
+++ /dev/null
@@ -1,58 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.VarChar
-  ( VarChar(..)
-  , makeEncoded
-  , makeUnencoded
-  , isVarchar
-  )
-where
-
-import qualified Data.Char as Char
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | Represents a single logical character in a variable name.
-data VarChar
-  = Encoded Char Char
-  -- ^ A percent encoded triple. Note that this represents three literal
-  -- characters in the input, even though logically we always treat it as one
-  -- character. The two arguments are the high and the low hexadecimal digits,
-  -- respectively. This representation intentially keeps track of their case,
-  -- so as to avoid confusing values like @%aa@ and @%AA@. You should use
-  -- @makeEncoded@ to build these values.
-  | Unencoded Char
-  -- ^ A literal unencoded character. You should use @makeUnencoded@ to build
-  -- these values.
-  deriving (Eq, TH.Lift, Show)
-
-
--- | Makes sure that both characters are valid hexadecimal digits. If they are,
--- returns an @Encoded@ character. Otherwise returns nothing.
-makeEncoded :: Char -> Char -> Maybe VarChar
-makeEncoded hi lo = if Char.isHexDigit hi && Char.isHexDigit lo
-  then Just $ Encoded hi lo
-  else Nothing
-
-
--- | Makes sure that the character passes the @isVarchar@ predicate. If it
--- does, returns an @Unencoded@ character. Otherwise returns nothing.
-makeUnencoded :: Char -> Maybe VarChar
-makeUnencoded char = if isVarchar char then Just $ Unencoded char else Nothing
-
-
--- | Returns true if the given character is in the @varchar@ range defined by
--- section 2.3 of the RFC. Note that this does not include the @pct-encoded@
--- part of the grammar because that requires multiple characters to match.
-isVarchar :: Char -> Bool
-isVarchar x = case x of
-  '_' -> True
-  _ -> isAlpha x || Char.isDigit x
-
-
--- | Returns true if the given character is in the @ALPHA@ range defined by
--- section 1.5 of the RFC.
-isAlpha :: Char -> Bool
-isAlpha x = Char.isAsciiUpper x || Char.isAsciiLower x
diff --git a/src/lib/Burrito/Type/Variable.hs b/src/lib/Burrito/Type/Variable.hs
deleted file mode 100644
--- a/src/lib/Burrito/Type/Variable.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-{-# LANGUAGE DeriveLift #-}
-
--- | Warning: This module is not considered part of Burrito's public API. As
--- such, it may change at any time. Use it with caution!.
-module Burrito.Type.Variable
-  ( Variable(..)
-  )
-where
-
-import qualified Burrito.Type.Modifier as Modifier
-import qualified Burrito.Type.Name as Name
-import qualified Language.Haskell.TH.Syntax as TH
-
-
--- | Represents a variable in an expression.
-data Variable = Variable
-  { modifier :: Modifier.Modifier
-  , name :: Name.Name
-  } deriving (Eq, TH.Lift, Show)
diff --git a/src/test/Main.hs b/src/test/Main.hs
--- a/src/test/Main.hs
+++ b/src/test/Main.hs
@@ -1,1330 +1,1469 @@
-{-# OPTIONS_GHC -Wno-orphans #-}
-
-{-# LANGUAGE OverloadedLists #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE TypeFamilies #-}
-
-module Main
-  ( main
-  )
-where
-
-import qualified Burrito
-import qualified Burrito.Type.LitChar as LitChar
-import qualified Burrito.Type.Expression as Expression
-import qualified Burrito.Type.Literal as Literal
-import qualified Burrito.Type.Modifier as Modifier
-import qualified Burrito.Type.Name as Name
-import qualified Burrito.Type.NonEmpty as NonEmpty
-import qualified Burrito.Type.Operator as Operator
-import qualified Burrito.Type.Template as Template
-import qualified Burrito.Type.Token as Token
-import qualified Burrito.Type.VarChar as VarChar
-import qualified Burrito.Type.Variable as Variable
-import qualified Data.Char as Char
-import qualified Data.Either as Either
-import qualified Data.Maybe as Maybe
-import qualified Data.String as String
-import qualified GHC.Exts as Exts
-import qualified GHC.Stack as Stack
-import qualified Test.Hspec as Test
-import qualified Test.QuickCheck as QC
-
-main :: IO ()
-main = Test.hspec . Test.describe "Burrito" $ do
-
-  Test.it "accepts empty templates" $ do
-    test "" [] ""
-
-  Test.it "ignores extra variables" $ do
-    test "" ["extra" =: "ignored"] ""
-
-  Test.it "accepts ascii literals" $ do
-    test "!" [] "!"
-    test "#" [] "#"
-    test "$" [] "$"
-    test "&" [] "&"
-    test "(" [] "("
-    test ")" [] ")"
-    test "*" [] "*"
-    test "+" [] "+"
-    test "," [] ","
-    test "-" [] "-"
-    test "." [] "."
-    test "/" [] "/"
-    test "0" [] "0"
-    test "9" [] "9"
-    test ":" [] ":"
-    test ";" [] ";"
-    test "=" [] "="
-    test "?" [] "?"
-    test "@" [] "@"
-    test "A" [] "A"
-    test "Z" [] "Z"
-    test "[" [] "["
-    test "]" [] "]"
-    test "_" [] "_"
-    test "a" [] "a"
-    test "z" [] "z"
-    test "~" [] "~"
-
-  Test.it "accepts unicode literals" $ do
-    test "\xa0" [] "%C2%A0"
-    test "\xd7ff" [] "%ED%9F%BF"
-    test "\xf900" [] "%EF%A4%80"
-    test "\xfdcf" [] "%EF%B7%8F"
-    test "\xfdf0" [] "%EF%B7%B0"
-    test "\xffef" [] "%EF%BF%AF"
-    test "\x10000" [] "%F0%90%80%80"
-    test "\x1fffd" [] "%F0%9F%BF%BD"
-    test "\x20000" [] "%F0%A0%80%80"
-    test "\x2fffd" [] "%F0%AF%BF%BD"
-    test "\x30000" [] "%F0%B0%80%80"
-    test "\x3fffd" [] "%F0%BF%BF%BD"
-    test "\x40000" [] "%F1%80%80%80"
-    test "\x4fffd" [] "%F1%8F%BF%BD"
-    test "\x50000" [] "%F1%90%80%80"
-    test "\x5fffd" [] "%F1%9F%BF%BD"
-    test "\x60000" [] "%F1%A0%80%80"
-    test "\x6fffd" [] "%F1%AF%BF%BD"
-    test "\x70000" [] "%F1%B0%80%80"
-    test "\x7fffd" [] "%F1%BF%BF%BD"
-    test "\x80000" [] "%F2%80%80%80"
-    test "\x8fffd" [] "%F2%8F%BF%BD"
-    test "\x90000" [] "%F2%90%80%80"
-    test "\x9fffd" [] "%F2%9F%BF%BD"
-    test "\xa0000" [] "%F2%A0%80%80"
-    test "\xafffd" [] "%F2%AF%BF%BD"
-    test "\xb0000" [] "%F2%B0%80%80"
-    test "\xbfffd" [] "%F2%BF%BF%BD"
-    test "\xc0000" [] "%F3%80%80%80"
-    test "\xcfffd" [] "%F3%8F%BF%BD"
-    test "\xd0000" [] "%F3%90%80%80"
-    test "\xdfffd" [] "%F3%9F%BF%BD"
-    test "\xe1000" [] "%F3%A1%80%80"
-    test "\xefffd" [] "%F3%AF%BF%BD"
-
-  Test.it "accepts private literals" $ do
-    test "\xe000" [] "%EE%80%80"
-    test "\xf8ff" [] "%EF%A3%BF"
-    test "\xf0000" [] "%F3%B0%80%80"
-    test "\xffffd" [] "%F3%BF%BF%BD"
-    test "\x100000" [] "%F4%80%80%80"
-    test "\x10fffd" [] "%F4%8F%BF%BD"
-
-  Test.it "passes percent encoded literals through" $ do
-    test "%00" [] "%00"
-
-  Test.it "normalizes percent encodings to uppercase" $ do
-    test "%AA" [] "%AA"
-    test "%Aa" [] "%AA"
-    test "%aA" [] "%AA"
-    test "%aa" [] "%AA"
-
-  Test.it "rejects invalid percent encodings" $ do
-    test "%" [] Failure
-    test "%0" [] Failure
-    test "%0z" [] Failure
-    test "%z" [] Failure
-
-  Test.it "does not decode percent encoded literals" $ do
-    test "%30" [] "%30"
-
-  Test.it "rejects invalid literals" $ do
-    test " " [] Failure
-    test "\"" [] Failure
-    test "'" [] Failure
-    test "%" [] Failure
-    test "<" [] Failure
-    test ">" [] Failure
-    test "\\" [] Failure
-    test "^" [] Failure
-    test "`" [] Failure
-    test "{" [] Failure
-    test "|" [] Failure
-    test "}" [] Failure
-
-  Test.it "rejects empty variable names" $ do
-    test "{}" [] Failure
-    test "{,}" [] Failure
-    test "{a,,b}" [] Failure
-    test "{+}" [] Failure
-    test "{:1}" [] Failure
-    test "{*}" [] Failure
-
-  Test.it "accepts uppercase variable names" $ do
-    test "{AZ}" [] ""
-
-  Test.it "accepts lowercase variable names" $ do
-    test "{az}" [] ""
-
-  Test.it "accepts decimal variable names" $ do
-    test "{09}" [] ""
-
-  Test.it "accepts underscores in variable names" $ do
-    test "{_a}" [] ""
-    test "{a_}" [] ""
-    test "{_}" [] ""
-
-  Test.it "accepts dots in variable names" $ do
-    test "{A.A}" [] ""
-    test "{a.a}" [] ""
-    test "{0.0}" [] ""
-    test "{_._}" [] ""
-    test "{%aa.%aa}" [] ""
-
-  Test.it "rejects invalid dots in variable names" $ do
-    test "{.}" [] Failure
-    test "{a.}" [] Failure
-    test "{+.a}" [] Failure
-    test "{a..b}" [] Failure
-
-  Test.it "accepts percent encoded variable names" $ do
-    test "{%00}" [] ""
-
-  -- It's unclear if percent encoded triplets in variable names should be case
-  -- sensitive or not. Section 2.3 says: "Variable names are case-sensitive
-  -- because the name might be expanded within a case-sensitive URI component."
-  -- But the HEXDIG rule in section 1.5 says: "; case-insensitive". I think
-  -- it's safe to assume they are case sensitive because they can appear in an
-  -- expansion. For example:
-  --
-  --    render "{;%aa}" [("%aa", "A")] ==> ";%aa=A"
-  --
-  -- However percent encoded triplets that only differ by case would be decoded
-  -- into the same octet anyway.
-  Test.it "does not normalize percent encoded variable names" $ do
-    test "{%AA}" ["%AA" =: "upper-upper"] "upper-upper"
-    test "{%Aa}" ["%Aa" =: "upper-lower"] "upper-lower"
-    test "{%aA}" ["%aA" =: "lower-upper"] "lower-upper"
-    test "{%aa}" ["%aa" =: "lower-lower"] "lower-lower"
-
-  Test.it "rejects invalid percent encoded variable names" $ do
-    test "{%}" [] Failure
-    test "{%0}" [] Failure
-    test "{%0z}" [] Failure
-    test "{%z}" [] Failure
-
-  Test.it "rejects invalid variable names" $ do
-    test "{!}" [] Failure
-
-  Test.it "rejects invalid expressions" $ do
-    test "{" [] Failure
-    test "{{}" [] Failure
-    test "}" [] Failure
-    test "{}}" [] Failure
-
-  Test.it "accepts multiple variables in one expression" $ do
-    test "{a,b}" [] ""
-    test "{a,b,c,d}" [] ""
-    test "{a,a}" [] ""
-
-  Test.it "accepts prefix modifiers" $ do
-    test "{a:5}" [] ""
-    test "{a:67}" [] ""
-    test "{a:801}" [] ""
-    test "{a:234}" [] ""
-    test "{a:9999}" [] ""
-
-  Test.it "applies prefix modifiers" $ do
-    let values = ["a" =: "abcdefghijklmnopqrstuvwxyz"] :: Values
-    test "{a:1}" values "a"
-    test "{a:5}" values "abcde"
-    test "{a:10}" values "abcdefghij"
-    test "{a:15}" values "abcdefghijklmno"
-    test "{a:20}" values "abcdefghijklmnopqrst"
-    test "{a:25}" values "abcdefghijklmnopqrstuvwxy"
-    test "{a:30}" values "abcdefghijklmnopqrstuvwxyz"
-
-  Test.it "rejects invalid prefix modifiers" $ do
-    test "{a:}" [] Failure
-    test "{a:0}" [] Failure
-    test "{a:10000}" [] Failure
-    test "{a:-1}" [] Failure
-
-  Test.it "accepts explode modifiers" $ do
-    test "{a*}" [] ""
-
-  Test.it "rejects both prefix and explode modifiers" $ do
-    test "{a:1*}" [] Failure
-    test "{a*:1}" [] Failure
-
-  Test.it "accepts different modifiers on different variables" $ do
-    test "{a,b:1,c*}" [] ""
-
-  Test.it "accepts allowed operators" $ do
-    test "{+a}" [] ""
-    test "{#a}" [] ""
-    test "{.a}" [] ""
-    test "{/a}" [] ""
-    test "{;a}" [] ""
-    test "{?a}" [] ""
-    test "{&a}" [] ""
-
-  Test.it "rejects reserved operators" $ do
-    test "{=a}" [] Failure
-    test "{,a}" [] Failure
-    test "{!a}" [] Failure
-    test "{@a}" [] Failure
-    test "{|a}" [] Failure
-
-  Test.it "rejects multiple operators" $ do
-    test "{+#a}" [] Failure
-
-  Test.it "rejects different operators for different variables" $ do
-    test "{+a,#b}" [] Failure
-
-  Test.it "accepts operators and modifiers" $ do
-    test "{+a:1}" [] ""
-    test "{#a*}" [] ""
-
-  Test.it "accepts multiple variables with an operator" $ do
-    test "{+a,b}" [] ""
-    test "{#a,b}" [] ""
-    test "{.a,b}" [] ""
-    test "{/a,b}" [] ""
-    test "{;a,b}" [] ""
-    test "{?a,b}" [] ""
-    test "{&a,b}" [] ""
-
-  Test.it "accepts multiple expressions" $ do
-    test "{a}{b}" [] ""
-    test "{a}{b}{c}{d}" [] ""
-    test "{a}{a}" [] ""
-
-  Test.it "rejects nested expressions" $ do
-    test "{{}}" [] Failure
-    test "{a{b}}" [] Failure
-    test "{{a}b}" [] Failure
-    test "{a{b}c}" [] Failure
-
-  Test.it "accepts literals and expressions together" $ do
-    test "a{b}" [] "a"
-    test "{a}b" [] "b"
-    test "a{b}c" [] "ac"
-    test "{a}b{c}" [] "b"
-
-  Test.it "handles missing values" $ do
-    test "{a}" [] ""
-    test "{+a}" [] ""
-    test "{#a}" [] ""
-    test "{.a}" [] ""
-    test "{/a}" [] ""
-    test "{;a}" [] ""
-    test "{?a}" [] ""
-    test "{&a}" [] ""
-
-  Test.it "handles empty list values" $ do
-    let values = ["a" =: emptyList] :: Values
-    test "{a}" values ""
-    test "{+a}" values ""
-    test "{#a}" values ""
-    test "{.a}" values ""
-    test "{/a}" values ""
-    test "{;a}" values ""
-    test "{?a}" values ""
-    test "{&a}" values ""
-
-  Test.it "handles empty dictionary values" $ do
-    let values = ["a" =: emptyDictionary] :: Values
-    test "{a}" values ""
-    test "{+a}" values ""
-    test "{#a}" values ""
-    test "{.a}" values ""
-    test "{/a}" values ""
-    test "{;a}" values ""
-    test "{?a}" values ""
-    test "{&a}" values ""
-
-  Test.it "handles empty string values" $ do
-    let values = ["a" =: ""] :: Values
-    test "{a}" values ""
-    test "{+a}" values ""
-    test "{#a}" values "#"
-    test "{.a}" values "."
-    test "{/a}" values "/"
-    test "{;a}" values ";a"
-    test "{?a}" values "?a="
-    test "{&a}" values "&a="
-
-  Test.it "handles nonempty string values" $ do
-    let values = ["a" =: "A"] :: Values
-    test "{a}" values "A"
-    test "{+a}" values "A"
-    test "{#a}" values "#A"
-    test "{.a}" values ".A"
-    test "{/a}" values "/A"
-    test "{;a}" values ";a=A"
-    test "{?a}" values "?a=A"
-    test "{&a}" values "&a=A"
-
-  Test.it "handles a mix of defined and undefined values" $ do
-    let values = ["b" =: "B"] :: Values
-    test "{a,b}" values "B"
-    test "{+a,b}" values "B"
-    test "{#a,b}" values "#B"
-    test "{.a,b}" values ".B"
-    test "{/a,b}" values "/B"
-    test "{;a,b}" values ";b=B"
-    test "{?a,b}" values "?b=B"
-    test "{&a,b}" values "&b=B"
-
-  Test.it "handles multiple empty string values" $ do
-    let values = ["a" =: ""] :: Values
-    test "{a,a}" values ","
-    test "{+a,a}" values ","
-    test "{#a,a}" values "#,"
-    test "{.a,a}" values ".."
-    test "{/a,a}" values "//"
-    test "{;a,a}" values ";a;a"
-    test "{?a,a}" values "?a=&a="
-    test "{&a,a}" values "&a=&a="
-
-  Test.it "handles multiple non-empty string values" $ do
-    let values = ["a" =: "A", "b" =: "B"] :: Values
-    test "{a,b}" values "A,B"
-    test "{+a,b}" values "A,B"
-    test "{#a,b}" values "#A,B"
-    test "{.a,b}" values ".A.B"
-    test "{/a,b}" values "/A/B"
-    test "{;a,b}" values ";a=A;b=B"
-    test "{?a,b}" values "?a=A&b=B"
-    test "{&a,b}" values "&a=A&b=B"
-
-  Test.it "escapes characters in composite dictionaries" $ do
-    let values = ["a" =: ["K! \xa0\xd7ff\x10000" =: "V! \xa0\xd7ff\x10000"]] :: Values
-    test "{a*}" values "K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
-    test "{+a*}" values "K!%20%C2%A0%ED%9F%BF%F0%90%80%80=V!%20%C2%A0%ED%9F%BF%F0%90%80%80"
-    test "{#a*}" values "#K!%20%C2%A0%ED%9F%BF%F0%90%80%80=V!%20%C2%A0%ED%9F%BF%F0%90%80%80"
-    test "{.a*}" values ".K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
-    test "{/a*}" values "/K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
-    test "{;a*}" values ";K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
-    test "{?a*}" values "?K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
-    test "{&a*}" values "&K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
-
-  Test.it "prefers the first variable" $ do
-    test "{a}" ["a" =: "A", "a" =: "B"] "A"
-    test "{a}" ["a" =: "B", "a" =: "A"] "B"
-
-  Test.describe "passes test from rfc" $ do
-
-    Test.it "section 1.1" $ do
-      test "http://example.com/~{username}/" ["username" =: "fred"] "http://example.com/~fred/"
-      test "http://example.com/~{username}/" ["username" =: "mark"] "http://example.com/~mark/"
-      test "http://example.com/dictionary/{term:1}/{term}" ["term" =: "cat"] "http://example.com/dictionary/c/cat"
-      test "http://example.com/dictionary/{term:1}/{term}" ["term" =: "dog"] "http://example.com/dictionary/d/dog"
-      test "http://example.com/search{?q,lang}" ["q" =: "cat", "lang" =: "en"] "http://example.com/search?q=cat&lang=en"
-      test "http://example.com/search{?q,lang}" ["q" =: "chien", "lang" =: "fr"] "http://example.com/search?q=chien&lang=fr"
-      test "http://www.example.com/foo{?query,number}" ["query" =: "mycelium", "number" =: "100"] "http://www.example.com/foo?query=mycelium&number=100"
-      test "http://www.example.com/foo{?query,number}" ["number" =: "100"] "http://www.example.com/foo?number=100"
-      test "http://www.example.com/foo{?query,number}" [] "http://www.example.com/foo"
-
-    Test.describe "section 1.2" $ do
-      let
-        values =
-          [ "empty" =: ""
-          , "hello" =: "Hello World!"
-          , "keys" =: ["semi" =: ";", "dot" =: ".", "comma" =: ","]
-          , "list" =: ["red", "green", "blue"]
-          , "path" =: "/foo/bar"
-          , "var" =: "value"
-          , "x" =: "1024"
-          , "y" =: "768"
-          ] :: Values
-
-      Test.it "level 1" $ do
-        test "{var}" values "value"
-        test "{hello}" values "Hello%20World%21"
-
-      Test.it "level 2" $ do
-        test "{+var}" values "value"
-        test "{+hello}" values "Hello%20World!"
-        test "{+path}/here" values "/foo/bar/here"
-        test "here?ref={+path}" values "here?ref=/foo/bar"
-        test "X{#var}" values "X#value"
-        test "X{#hello}" values "X#Hello%20World!"
-
-      Test.it "level 3" $ do
-        test "map?{x,y}" values "map?1024,768"
-        test "{x,hello,y}" values "1024,Hello%20World%21,768"
-        test "{+x,hello,y}" values "1024,Hello%20World!,768"
-        test "{+path,x}/here" values "/foo/bar,1024/here"
-        test "{#x,hello,y}" values "#1024,Hello%20World!,768"
-        test "{#path,x}/here" values "#/foo/bar,1024/here"
-        test "X{.var}" values "X.value"
-        test "X{.x,y}" values "X.1024.768"
-        test "{/var}" values "/value"
-        test "{/var,x}/here" values "/value/1024/here"
-        test "{;x,y}" values ";x=1024;y=768"
-        test "{;x,y,empty}" values ";x=1024;y=768;empty"
-        test "{?x,y}" values "?x=1024&y=768"
-        test "{?x,y,empty}" values "?x=1024&y=768&empty="
-        test "?fixed=yes{&x}" values "?fixed=yes&x=1024"
-        test "{&x,y,empty}" values "&x=1024&y=768&empty="
-
-      Test.it "level 4" $ do
-        test "{var:3}" values "val"
-        test "{var:30}" values "value"
-        test "{list}" values "red,green,blue"
-        test "{list*}" values "red,green,blue"
-        test "{keys}" values "semi,%3B,dot,.,comma,%2C"
-        test "{keys*}" values "semi=%3B,dot=.,comma=%2C"
-        test "{+path:6}/here" values "/foo/b/here"
-        test "{+list}" values "red,green,blue"
-        test "{+list*}" values "red,green,blue"
-        test "{+keys}" values "semi,;,dot,.,comma,,"
-        test "{+keys*}" values "semi=;,dot=.,comma=,"
-        test "{#path:6}/here" values "#/foo/b/here"
-        test "{#list}" values "#red,green,blue"
-        test "{#list*}" values "#red,green,blue"
-        test "{#keys}" values "#semi,;,dot,.,comma,,"
-        test "{#keys*}" values "#semi=;,dot=.,comma=,"
-        test "X{.var:3}" values "X.val"
-        test "X{.list}" values "X.red,green,blue"
-        test "X{.list*}" values "X.red.green.blue"
-        test "X{.keys}" values "X.semi,%3B,dot,.,comma,%2C"
-        test "X{.keys*}" values "X.semi=%3B.dot=..comma=%2C"
-        test "{/var:1,var}" values "/v/value"
-        test "{/list}" values "/red,green,blue"
-        test "{/list*}" values "/red/green/blue"
-        test "{/list*,path:4}" values "/red/green/blue/%2Ffoo"
-        test "{/keys}" values "/semi,%3B,dot,.,comma,%2C"
-        test "{/keys*}" values "/semi=%3B/dot=./comma=%2C"
-        test "{;hello:5}" values ";hello=Hello"
-        test "{;list}" values ";list=red,green,blue"
-        test "{;list*}" values ";list=red;list=green;list=blue"
-        test "{;keys}" values ";keys=semi,%3B,dot,.,comma,%2C"
-        test "{;keys*}" values ";semi=%3B;dot=.;comma=%2C"
-        test "{?var:3}" values "?var=val"
-        test "{?list}" values "?list=red,green,blue"
-        test "{?list*}" values "?list=red&list=green&list=blue"
-        test "{?keys}" values "?keys=semi,%3B,dot,.,comma,%2C"
-        test "{?keys*}" values "?semi=%3B&dot=.&comma=%2C"
-        test "{&var:3}" values "&var=val"
-        test "{&list}" values "&list=red,green,blue"
-        test "{&list*}" values "&list=red&list=green&list=blue"
-        test "{&keys}" values "&keys=semi,%3B,dot,.,comma,%2C"
-        test "{&keys*}" values "&semi=%3B&dot=.&comma=%2C"
-
-    Test.it "section 2.4.1" $ do
-      let values = ["var" =: "value", "semi" =: ";"] :: Values
-      test "{var}" values "value"
-      test "{var:20}" values "value"
-      test "{var:3}" values "val"
-      test "{semi}" values "%3B"
-      test "{semi:2}" values "%3B"
-
-    Test.it "section 2.4.2" $ do
-      let
-        values =
-          ["year" =: ["1965", "2000", "2012"], "dom" =: ["example", "com"]] :: Values
-      test "find{?year*}" values "find?year=1965&year=2000&year=2012"
-      test "www{.dom*}" values "www.example.com"
-
-    Test.describe "section 3.1" $ do
-      let
-        values =
-          [ "base" =: "http://example.com/home/"
-          , "count" =: ["one", "two", "three"]
-          , "dom" =: ["example", "com"]
-          , "dub" =: "me/too"
-          , "empty_keys" =: emptyDictionary
-          , "empty" =: ""
-          , "half" =: "50%"
-          , "hello" =: "Hello World!"
-          , "keys" =: ["semi" =: ";", "dot" =: ".", "comma" =: ","]
-          , "list" =: ["red", "green", "blue"]
-          , "path" =: "/foo/bar"
-          , "v" =: "6"
-          , "var" =: "value"
-          , "who" =: "fred"
-          , "x" =: "1024"
-          , "y" =: "768"
-          ] :: Values
-
-      Test.it "subsection 1" $ do
-        test "{count}" values "one,two,three"
-        test "{count*}" values "one,two,three"
-        test "{/count}" values "/one,two,three"
-        test "{/count*}" values "/one/two/three"
-        test "{;count}" values ";count=one,two,three"
-        test "{;count*}" values ";count=one;count=two;count=three"
-        test "{?count}" values "?count=one,two,three"
-        test "{?count*}" values "?count=one&count=two&count=three"
-        test "{&count*}" values "&count=one&count=two&count=three"
-
-      Test.it "subsection 2" $ do
-        test "{var}" values "value"
-        test "{hello}" values "Hello%20World%21"
-        test "{half}" values "50%25"
-        test "O{empty}X" values "OX"
-        test "O{undef}X" values "OX"
-        test "{x,y}" values "1024,768"
-        test "{x,hello,y}" values "1024,Hello%20World%21,768"
-        test "?{x,empty}" values "?1024,"
-        test "?{x,undef}" values "?1024"
-        test "?{undef,y}" values "?768"
-        test "{var:3}" values "val"
-        test "{var:30}" values "value"
-        test "{list}" values "red,green,blue"
-        test "{list*}" values "red,green,blue"
-        test "{keys}" values "semi,%3B,dot,.,comma,%2C"
-        test "{keys*}" values "semi=%3B,dot=.,comma=%2C"
-
-      Test.it "subsection 3" $ do
-        test "{+var}" values "value"
-        test "{+hello}" values "Hello%20World!"
-        test "{+half}" values "50%25"
-        test "{base}index" values "http%3A%2F%2Fexample.com%2Fhome%2Findex"
-        test "{+base}index" values "http://example.com/home/index"
-        test "O{+empty}X" values "OX"
-        test "O{+undef}X" values "OX"
-        test "{+path}/here" values "/foo/bar/here"
-        test "here?ref={+path}" values "here?ref=/foo/bar"
-        test "up{+path}{var}/here" values "up/foo/barvalue/here"
-        test "{+x,hello,y}" values "1024,Hello%20World!,768"
-        test "{+path,x}/here" values "/foo/bar,1024/here"
-        test "{+path:6}/here" values "/foo/b/here"
-        test "{+list}" values "red,green,blue"
-        test "{+list*}" values "red,green,blue"
-        test "{+keys}" values "semi,;,dot,.,comma,,"
-        test "{+keys*}" values "semi=;,dot=.,comma=,"
-
-      Test.it "subsection 4" $ do
-        test "{#var}" values "#value"
-        test "{#hello}" values "#Hello%20World!"
-        test "{#half}" values "#50%25"
-        test "foo{#empty}" values "foo#"
-        test "foo{#undef}" values "foo"
-        test "{#x,hello,y}" values "#1024,Hello%20World!,768"
-        test "{#path,x}/here" values "#/foo/bar,1024/here"
-        test "{#path:6}/here" values "#/foo/b/here"
-        test "{#list}" values "#red,green,blue"
-        test "{#list*}" values "#red,green,blue"
-        test "{#keys}" values "#semi,;,dot,.,comma,,"
-        test "{#keys*}" values "#semi=;,dot=.,comma=,"
-
-      Test.it "subsection 5" $ do
-        test "{.who}" values ".fred"
-        test "{.who,who}" values ".fred.fred"
-        test "{.half,who}" values ".50%25.fred"
-        test "www{.dom*}" values "www.example.com"
-        test "X{.var}" values "X.value"
-        test "X{.empty}" values "X."
-        test "X{.undef}" values "X"
-        test "X{.var:3}" values "X.val"
-        test "X{.list}" values "X.red,green,blue"
-        test "X{.list*}" values "X.red.green.blue"
-        test "X{.keys}" values "X.semi,%3B,dot,.,comma,%2C"
-        test "X{.keys*}" values "X.semi=%3B.dot=..comma=%2C"
-        test "X{.empty_keys}" values "X"
-        test "X{.empty_keys*}" values "X"
-
-      Test.it "subsection 6" $ do
-        test "{/who}" values "/fred"
-        test "{/who,who}" values "/fred/fred"
-        test "{/half,who}" values "/50%25/fred"
-        test "{/who,dub}" values "/fred/me%2Ftoo"
-        test "{/var}" values "/value"
-        test "{/var,empty}" values "/value/"
-        test "{/var,undef}" values "/value"
-        test "{/var,x}/here" values "/value/1024/here"
-        test "{/var:1,var}" values "/v/value"
-        test "{/list}" values "/red,green,blue"
-        test "{/list*}" values "/red/green/blue"
-        test "{/list*,path:4}" values "/red/green/blue/%2Ffoo"
-        test "{/keys}" values "/semi,%3B,dot,.,comma,%2C"
-        test "{/keys*}" values "/semi=%3B/dot=./comma=%2C"
-
-      Test.it "subsection 7" $ do
-        test "{;who}" values ";who=fred"
-        test "{;half}" values ";half=50%25"
-        test "{;empty}" values ";empty"
-        test "{;v,empty,who}" values ";v=6;empty;who=fred"
-        test "{;v,bar,who}" values ";v=6;who=fred"
-        test "{;x,y}" values ";x=1024;y=768"
-        test "{;x,y,empty}" values ";x=1024;y=768;empty"
-        test "{;x,y,undef}" values ";x=1024;y=768"
-        test "{;hello:5}" values ";hello=Hello"
-        test "{;list}" values ";list=red,green,blue"
-        test "{;list*}" values ";list=red;list=green;list=blue"
-        test "{;keys}" values ";keys=semi,%3B,dot,.,comma,%2C"
-        test "{;keys*}" values ";semi=%3B;dot=.;comma=%2C"
-
-      Test.it "subsection 8" $ do
-        test "{?who}" values "?who=fred"
-        test "{?half}" values "?half=50%25"
-        test "{?x,y}" values "?x=1024&y=768"
-        test "{?x,y,empty}" values "?x=1024&y=768&empty="
-        test "{?x,y,undef}" values "?x=1024&y=768"
-        test "{?var:3}" values "?var=val"
-        test "{?list}" values "?list=red,green,blue"
-        test "{?list*}" values "?list=red&list=green&list=blue"
-        test "{?keys}" values "?keys=semi,%3B,dot,.,comma,%2C"
-        test "{?keys*}" values "?semi=%3B&dot=.&comma=%2C"
-
-      Test.it "subsection 9" $ do
-        test "{&who}" values "&who=fred"
-        test "{&half}" values "&half=50%25"
-        test "?fixed=yes{&x}" values "?fixed=yes&x=1024"
-        test "{&x,y,empty}" values "&x=1024&y=768&empty="
-        test "{&x,y,undef}" values "&x=1024&y=768"
-        test "{&var:3}" values "&var=val"
-        test "{&list}" values "&list=red,green,blue"
-        test "{&list*}" values "&list=red&list=green&list=blue"
-        test "{&keys}" values "&keys=semi,%3B,dot,.,comma,%2C"
-        test "{&keys*}" values "&semi=%3B&dot=.&comma=%2C"
-
-  Test.it "handles simple expansion" $ do
-    test "{a}" [] ""
-    test "{a}" ["a" =: emptyList] ""
-    test "{a}" ["a" =: emptyDictionary] ""
-    test "{a}" ["a" =: ""] ""
-    test "{a}" ["a" =: "A"] "A"
-    test "{a}" ["a" =: "~"] "~"
-    test "{a}" ["a" =: "%"] "%25"
-    test "{a}" ["a" =: "?"] "%3F"
-    test "{a}" ["a" =: "&"] "%26"
-    test "{a}" ["a" =: "\xa0"] "%C2%A0"
-    test "{a}" ["a" =: "\xd7ff"] "%ED%9F%BF"
-    test "{a}" ["a" =: "\x10000"] "%F0%90%80%80"
-    test "{a}" ["a" =: ["A"]] "A"
-    test "{a}" ["a" =: ["A", "B"]] "A,B"
-    test "{a}" ["a" =: ["%"]] "%25"
-    test "{a}" ["a" =: ["\xa0"]] "%C2%A0"
-    test "{a}" ["a" =: ["A" =: "1"]] "A,1"
-    test "{a}" ["a" =: ["A" =: "1", "B" =: "2"]] "A,1,B,2"
-    test "{a}" ["a" =: ["A" =: "%"]] "A,%25"
-    test "{a}" ["a" =: ["A" =: "\xa0"]] "A,%C2%A0"
-    test "{a}" ["a" =: ["%" =: "1"]] "%25,1"
-    test "{a*}" [] ""
-    test "{a*}" ["a" =: ""] ""
-    test "{a*}" ["a" =: "A"] "A"
-    test "{a*}" ["a" =: emptyList] ""
-    test "{a*}" ["a" =: ["A"]] "A"
-    test "{a*}" ["a" =: ["A", "B"]] "A,B"
-    test "{a*}" ["a" =: emptyDictionary] ""
-    test "{a*}" ["a" =: ["A" =: "1"]] "A=1"
-    test "{a*}" ["a" =: ["A" =: "1", "B" =: "2"]] "A=1,B=2"
-    test "{a:1}" [] ""
-    test "{a:1}" ["a" =: ""] ""
-    test "{a:1}" ["a" =: "A"] "A"
-    test "{a:1}" ["a" =: "AB"] "A"
-    test "{a:1}" ["a" =: "%B"] "%25"
-    test "{a:1}" ["a" =: "\xa0\&B"] "%C2%A0"
-    test "{a:1}" ["a" =: "\xd7ff\&B"] "%ED%9F%BF"
-    test "{a:1}" ["a" =: "\x10000\&B"] "%F0%90%80%80"
-    test "{a:1}" ["a" =: emptyList] ""
-    test "{a:1}" ["a" =: ["AB"]] "AB"
-    test "{a:1}" ["a" =: ["AB", "CD"]] "AB,CD"
-    test "{a:1}" ["a" =: emptyDictionary] ""
-    test "{a:1}" ["a" =: ["AB" =: "12"]] "AB,12"
-    test "{a:1}" ["a" =: ["AB" =: "12", "CD" =: "34"]] "AB,12,CD,34"
-    test "{a,a}" [] ""
-    test "{a,a}" ["a" =: emptyList] ""
-    test "{a,a}" ["a" =: emptyDictionary] ""
-    test "{a,a}" ["a" =: ""] ","
-    test "{a,b}" ["a" =: ""] ""
-    test "{a,b}" ["b" =: ""] ""
-    test "{%aa}" ["%aa" =: "A"] "A"
-    test "{%aa}" ["%aa" =: ["A", "B"]] "A,B"
-    test "{%aa}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "A,1,B,2"
-    test "{%aa*}" ["%aa" =: "A"] "A"
-    test "{%aa*}" ["%aa" =: ["A", "B"]] "A,B"
-    test "{%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "A=1,B=2"
-
-  Test.it "handles reserved expansion" $ do
-    test "{+a}" [] ""
-    test "{+a}" ["a" =: emptyList] ""
-    test "{+a}" ["a" =: emptyDictionary] ""
-    test "{+a}" ["a" =: ""] ""
-    test "{+a}" ["a" =: "A"] "A"
-    test "{+a}" ["a" =: "~"] "~"
-    test "{+a}" ["a" =: "%"] "%25"
-    test "{+a}" ["a" =: "?"] "?"
-    test "{+a}" ["a" =: "&"] "&"
-    test "{+a}" ["a" =: "\xa0"] "%C2%A0"
-    test "{+a}" ["a" =: "\xd7ff"] "%ED%9F%BF"
-    test "{+a}" ["a" =: "\x10000"] "%F0%90%80%80"
-    test "{+a}" ["a" =: ["A"]] "A"
-    test "{+a}" ["a" =: ["A", "B"]] "A,B"
-    test "{+a}" ["a" =: ["%"]] "%25"
-    test "{+a}" ["a" =: ["\xa0"]] "%C2%A0"
-    test "{+a}" ["a" =: ["A" =: "1"]] "A,1"
-    test "{+a}" ["a" =: ["A" =: "1", "B" =: "2"]] "A,1,B,2"
-    test "{+a}" ["a" =: ["A" =: "%"]] "A,%25"
-    test "{+a}" ["a" =: ["A" =: "\xa0"]] "A,%C2%A0"
-    test "{+a}" ["a" =: ["%" =: "1"]] "%25,1"
-    test "{+a*}" [] ""
-    test "{+a*}" ["a" =: ""] ""
-    test "{+a*}" ["a" =: "A"] "A"
-    test "{+a*}" ["a" =: emptyList] ""
-    test "{+a*}" ["a" =: ["A"]] "A"
-    test "{+a*}" ["a" =: ["A", "B"]] "A,B"
-    test "{+a*}" ["a" =: emptyDictionary] ""
-    test "{+a*}" ["a" =: ["A" =: "1"]] "A=1"
-    test "{+a*}" ["a" =: ["A" =: "1", "B" =: "2"]] "A=1,B=2"
-    test "{+a:1}" [] ""
-    test "{+a:1}" ["a" =: ""] ""
-    test "{+a:1}" ["a" =: "A"] "A"
-    test "{+a:1}" ["a" =: "AB"] "A"
-    test "{+a:1}" ["a" =: "%B"] "%25"
-    test "{+a:1}" ["a" =: "\xa0\&B"] "%C2%A0"
-    test "{+a:1}" ["a" =: "\xd7ff\&B"] "%ED%9F%BF"
-    test "{+a:1}" ["a" =: "\x10000\&B"] "%F0%90%80%80"
-    test "{+a:1}" ["a" =: emptyList] ""
-    test "{+a:1}" ["a" =: ["AB"]] "AB"
-    test "{+a:1}" ["a" =: ["AB", "CD"]] "AB,CD"
-    test "{+a:1}" ["a" =: emptyDictionary] ""
-    test "{+a:1}" ["a" =: ["AB" =: "12"]] "AB,12"
-    test "{+a:1}" ["a" =: ["AB" =: "12", "CD" =: "34"]] "AB,12,CD,34"
-    test "{+a,a}" [] ""
-    test "{+a,a}" ["a" =: emptyList] ""
-    test "{+a,a}" ["a" =: emptyDictionary] ""
-    test "{+a,a}" ["a" =: ""] ","
-    test "{+a,b}" ["a" =: ""] ""
-    test "{+a,b}" ["b" =: ""] ""
-    test "{+%aa}" ["%aa" =: "A"] "A"
-    test "{+%aa}" ["%aa" =: ["A", "B"]] "A,B"
-    test "{+%aa}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "A,1,B,2"
-    test "{+%aa*}" ["%aa" =: "A"] "A"
-    test "{+%aa*}" ["%aa" =: ["A", "B"]] "A,B"
-    test "{+%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "A=1,B=2"
-
-  Test.it "handles fragment expansion" $ do
-    test "{#a}" [] ""
-    test "{#a}" ["a" =: emptyList] ""
-    test "{#a}" ["a" =: emptyDictionary] ""
-    test "{#a}" ["a" =: ""] "#"
-    test "{#a}" ["a" =: "A"] "#A"
-    test "{#a}" ["a" =: "~"] "#~"
-    test "{#a}" ["a" =: "%"] "#%25"
-    test "{#a}" ["a" =: "?"] "#?"
-    test "{#a}" ["a" =: "&"] "#&"
-    test "{#a}" ["a" =: "\xa0"] "#%C2%A0"
-    test "{#a}" ["a" =: "\xd7ff"] "#%ED%9F%BF"
-    test "{#a}" ["a" =: "\x10000"] "#%F0%90%80%80"
-    test "{#a}" ["a" =: ["A"]] "#A"
-    test "{#a}" ["a" =: ["A", "B"]] "#A,B"
-    test "{#a}" ["a" =: ["%"]] "#%25"
-    test "{#a}" ["a" =: ["\xa0"]] "#%C2%A0"
-    test "{#a}" ["a" =: ["A" =: "1"]] "#A,1"
-    test "{#a}" ["a" =: ["A" =: "1", "B" =: "2"]] "#A,1,B,2"
-    test "{#a}" ["a" =: ["A" =: "%"]] "#A,%25"
-    test "{#a}" ["a" =: ["A" =: "\xa0"]] "#A,%C2%A0"
-    test "{#a}" ["a" =: ["%" =: "1"]] "#%25,1"
-    test "{#a*}" [] ""
-    test "{#a*}" ["a" =: ""] "#"
-    test "{#a*}" ["a" =: "A"] "#A"
-    test "{#a*}" ["a" =: emptyList] ""
-    test "{#a*}" ["a" =: ["A"]] "#A"
-    test "{#a*}" ["a" =: ["A", "B"]] "#A,B"
-    test "{#a*}" ["a" =: emptyDictionary] ""
-    test "{#a*}" ["a" =: ["A" =: "1"]] "#A=1"
-    test "{#a*}" ["a" =: ["A" =: "1", "B" =: "2"]] "#A=1,B=2"
-    test "{#a:1}" [] ""
-    test "{#a:1}" ["a" =: ""] "#"
-    test "{#a:1}" ["a" =: "A"] "#A"
-    test "{#a:1}" ["a" =: "AB"] "#A"
-    test "{#a:1}" ["a" =: "%B"] "#%25"
-    test "{#a:1}" ["a" =: "\xa0\&B"] "#%C2%A0"
-    test "{#a:1}" ["a" =: "\xd7ff\&B"] "#%ED%9F%BF"
-    test "{#a:1}" ["a" =: "\x10000\&B"] "#%F0%90%80%80"
-    test "{#a:1}" ["a" =: emptyList] ""
-    test "{#a:1}" ["a" =: ["AB"]] "#AB"
-    test "{#a:1}" ["a" =: ["AB", "CD"]] "#AB,CD"
-    test "{#a:1}" ["a" =: emptyDictionary] ""
-    test "{#a:1}" ["a" =: ["AB" =: "12"]] "#AB,12"
-    test "{#a:1}" ["a" =: ["AB" =: "12", "CD" =: "34"]] "#AB,12,CD,34"
-    test "{#a,a}" [] ""
-    test "{#a,a}" ["a" =: emptyList] ""
-    test "{#a,a}" ["a" =: emptyDictionary] ""
-    test "{#a,a}" ["a" =: ""] "#,"
-    test "{#a,b}" ["a" =: ""] "#"
-    test "{#a,b}" ["b" =: ""] "#"
-    test "{#%aa}" ["%aa" =: "A"] "#A"
-    test "{#%aa}" ["%aa" =: ["A", "B"]] "#A,B"
-    test "{#%aa}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "#A,1,B,2"
-    test "{#%aa*}" ["%aa" =: "A"] "#A"
-    test "{#%aa*}" ["%aa" =: ["A", "B"]] "#A,B"
-    test "{#%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "#A=1,B=2"
-
-  Test.it "handles Test.it expansion" $ do
-    test "{.a}" [] ""
-    test "{.a}" ["a" =: emptyList] ""
-    test "{.a}" ["a" =: emptyDictionary] ""
-    test "{.a}" ["a" =: ""] "."
-    test "{.a}" ["a" =: "A"] ".A"
-    test "{.a}" ["a" =: "~"] ".~"
-    test "{.a}" ["a" =: "%"] ".%25"
-    test "{.a}" ["a" =: "?"] ".%3F"
-    test "{.a}" ["a" =: "&"] ".%26"
-    test "{.a}" ["a" =: "\xa0"] ".%C2%A0"
-    test "{.a}" ["a" =: "\xd7ff"] ".%ED%9F%BF"
-    test "{.a}" ["a" =: "\x10000"] ".%F0%90%80%80"
-    test "{.a}" ["a" =: ["A"]] ".A"
-    test "{.a}" ["a" =: ["A", "B"]] ".A,B"
-    test "{.a}" ["a" =: ["%"]] ".%25"
-    test "{.a}" ["a" =: ["\xa0"]] ".%C2%A0"
-    test "{.a}" ["a" =: ["A" =: "1"]] ".A,1"
-    test "{.a}" ["a" =: ["A" =: "1", "B" =: "2"]] ".A,1,B,2"
-    test "{.a}" ["a" =: ["A" =: "%"]] ".A,%25"
-    test "{.a}" ["a" =: ["A" =: "\xa0"]] ".A,%C2%A0"
-    test "{.a}" ["a" =: ["%" =: "1"]] ".%25,1"
-    test "{.a*}" [] ""
-    test "{.a*}" ["a" =: ""] "."
-    test "{.a*}" ["a" =: "A"] ".A"
-    test "{.a*}" ["a" =: emptyList] ""
-    test "{.a*}" ["a" =: ["A"]] ".A"
-    test "{.a*}" ["a" =: ["A", "B"]] ".A.B"
-    test "{.a*}" ["a" =: emptyDictionary] ""
-    test "{.a*}" ["a" =: ["A" =: "1"]] ".A=1"
-    test "{.a*}" ["a" =: ["A" =: "1", "B" =: "2"]] ".A=1.B=2"
-    test "{.a:1}" [] ""
-    test "{.a:1}" ["a" =: ""] "."
-    test "{.a:1}" ["a" =: "A"] ".A"
-    test "{.a:1}" ["a" =: "AB"] ".A"
-    test "{.a:1}" ["a" =: "%B"] ".%25"
-    test "{.a:1}" ["a" =: "\xa0\&B"] ".%C2%A0"
-    test "{.a:1}" ["a" =: "\xd7ff\&B"] ".%ED%9F%BF"
-    test "{.a:1}" ["a" =: "\x10000\&B"] ".%F0%90%80%80"
-    test "{.a:1}" ["a" =: emptyList] ""
-    test "{.a:1}" ["a" =: ["AB"]] ".AB"
-    test "{.a:1}" ["a" =: ["AB", "CD"]] ".AB,CD"
-    test "{.a:1}" ["a" =: emptyDictionary] ""
-    test "{.a:1}" ["a" =: ["AB" =: "12"]] ".AB,12"
-    test "{.a:1}" ["a" =: ["AB" =: "12", "CD" =: "34"]] ".AB,12,CD,34"
-    test "{.a,a}" [] ""
-    test "{.a,a}" ["a" =: emptyList] ""
-    test "{.a,a}" ["a" =: emptyDictionary] ""
-    test "{.a,a}" ["a" =: ""] ".."
-    test "{.a,b}" ["a" =: ""] "."
-    test "{.a,b}" ["b" =: ""] "."
-    test "{.%aa}" ["%aa" =: "A"] ".A"
-    test "{.%aa}" ["%aa" =: ["A", "B"]] ".A,B"
-    test "{.%aa}" ["%aa" =: ["A" =: "1", "B" =: "2"]] ".A,1,B,2"
-    test "{.%aa*}" ["%aa" =: "A"] ".A"
-    test "{.%aa*}" ["%aa" =: ["A", "B"]] ".A.B"
-    test "{.%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] ".A=1.B=2"
-
-  Test.it "handles segment expansion" $ do
-    test "{/a}" [] ""
-    test "{/a}" ["a" =: emptyList] ""
-    test "{/a}" ["a" =: emptyDictionary] ""
-    test "{/a}" ["a" =: ""] "/"
-    test "{/a}" ["a" =: "A"] "/A"
-    test "{/a}" ["a" =: "~"] "/~"
-    test "{/a}" ["a" =: "%"] "/%25"
-    test "{/a}" ["a" =: "?"] "/%3F"
-    test "{/a}" ["a" =: "&"] "/%26"
-    test "{/a}" ["a" =: "\xa0"] "/%C2%A0"
-    test "{/a}" ["a" =: "\xd7ff"] "/%ED%9F%BF"
-    test "{/a}" ["a" =: "\x10000"] "/%F0%90%80%80"
-    test "{/a}" ["a" =: ["A"]] "/A"
-    test "{/a}" ["a" =: ["A", "B"]] "/A,B"
-    test "{/a}" ["a" =: ["%"]] "/%25"
-    test "{/a}" ["a" =: ["\xa0"]] "/%C2%A0"
-    test "{/a}" ["a" =: ["A" =: "1"]] "/A,1"
-    test "{/a}" ["a" =: ["A" =: "1", "B" =: "2"]] "/A,1,B,2"
-    test "{/a}" ["a" =: ["A" =: "%"]] "/A,%25"
-    test "{/a}" ["a" =: ["A" =: "\xa0"]] "/A,%C2%A0"
-    test "{/a}" ["a" =: ["%" =: "1"]] "/%25,1"
-    test "{/a*}" [] ""
-    test "{/a*}" ["a" =: ""] "/"
-    test "{/a*}" ["a" =: "A"] "/A"
-    test "{/a*}" ["a" =: emptyList] ""
-    test "{/a*}" ["a" =: ["A"]] "/A"
-    test "{/a*}" ["a" =: ["A", "B"]] "/A/B"
-    test "{/a*}" ["a" =: emptyDictionary] ""
-    test "{/a*}" ["a" =: ["A" =: "1"]] "/A=1"
-    test "{/a*}" ["a" =: ["A" =: "1", "B" =: "2"]] "/A=1/B=2"
-    test "{/a:1}" [] ""
-    test "{/a:1}" ["a" =: ""] "/"
-    test "{/a:1}" ["a" =: "A"] "/A"
-    test "{/a:1}" ["a" =: "AB"] "/A"
-    test "{/a:1}" ["a" =: "%B"] "/%25"
-    test "{/a:1}" ["a" =: "\xa0\&B"] "/%C2%A0"
-    test "{/a:1}" ["a" =: "\xd7ff\&B"] "/%ED%9F%BF"
-    test "{/a:1}" ["a" =: "\x10000\&B"] "/%F0%90%80%80"
-    test "{/a:1}" ["a" =: emptyList] ""
-    test "{/a:1}" ["a" =: ["AB"]] "/AB"
-    test "{/a:1}" ["a" =: ["AB", "CD"]] "/AB,CD"
-    test "{/a:1}" ["a" =: emptyDictionary] ""
-    test "{/a:1}" ["a" =: ["AB" =: "12"]] "/AB,12"
-    test "{/a:1}" ["a" =: ["AB" =: "12", "CD" =: "34"]] "/AB,12,CD,34"
-    test "{/a,a}" [] ""
-    test "{/a,a}" ["a" =: emptyList] ""
-    test "{/a,a}" ["a" =: emptyDictionary] ""
-    test "{/a,a}" ["a" =: ""] "//"
-    test "{/a,b}" ["a" =: ""] "/"
-    test "{/a,b}" ["b" =: ""] "/"
-    test "{/%aa}" ["%aa" =: "A"] "/A"
-    test "{/%aa}" ["%aa" =: ["A", "B"]] "/A,B"
-    test "{/%aa}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "/A,1,B,2"
-    test "{/%aa*}" ["%aa" =: "A"] "/A"
-    test "{/%aa*}" ["%aa" =: ["A", "B"]] "/A/B"
-    test "{/%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "/A=1/B=2"
-
-  Test.it "handles parameter expansion" $ do
-    test "{;a}" [] ""
-    test "{;a}" ["a" =: emptyList] ""
-    test "{;a}" ["a" =: emptyDictionary] ""
-    test "{;a}" ["a" =: ""] ";a"
-    test "{;a}" ["a" =: "A"] ";a=A"
-    test "{;a}" ["a" =: "~"] ";a=~"
-    test "{;a}" ["a" =: "%"] ";a=%25"
-    test "{;a}" ["a" =: "?"] ";a=%3F"
-    test "{;a}" ["a" =: "&"] ";a=%26"
-    test "{;a}" ["a" =: "\xa0"] ";a=%C2%A0"
-    test "{;a}" ["a" =: "\xd7ff"] ";a=%ED%9F%BF"
-    test "{;a}" ["a" =: "\x10000"] ";a=%F0%90%80%80"
-    test "{;a}" ["a" =: ["A"]] ";a=A"
-    test "{;a}" ["a" =: ["A", "B"]] ";a=A,B"
-    test "{;a}" ["a" =: ["%"]] ";a=%25"
-    test "{;a}" ["a" =: ["\xa0"]] ";a=%C2%A0"
-    test "{;a}" ["a" =: ["A" =: "1"]] ";a=A,1"
-    test "{;a}" ["a" =: ["A" =: "1", "B" =: "2"]] ";a=A,1,B,2"
-    test "{;a}" ["a" =: ["A" =: "%"]] ";a=A,%25"
-    test "{;a}" ["a" =: ["A" =: "\xa0"]] ";a=A,%C2%A0"
-    test "{;a}" ["a" =: ["%" =: "1"]] ";a=%25,1"
-    test "{;a*}" [] ""
-    test "{;a*}" ["a" =: ""] ";a"
-    test "{;a*}" ["a" =: "A"] ";a=A"
-    test "{;a*}" ["a" =: emptyList] ""
-    test "{;a*}" ["a" =: ["A"]] ";a=A"
-    test "{;a*}" ["a" =: ["A", "B"]] ";a=A;a=B"
-    test "{;a*}" ["a" =: emptyDictionary] ""
-    test "{;a*}" ["a" =: ["A" =: "1"]] ";A=1"
-    test "{;a*}" ["a" =: ["A" =: "1", "B" =: "2"]] ";A=1;B=2"
-    test "{;a:1}" [] ""
-    test "{;a:1}" ["a" =: ""] ";a"
-    test "{;a:1}" ["a" =: "A"] ";a=A"
-    test "{;a:1}" ["a" =: "AB"] ";a=A"
-    test "{;a:1}" ["a" =: "%B"] ";a=%25"
-    test "{;a:1}" ["a" =: "\xa0\&B"] ";a=%C2%A0"
-    test "{;a:1}" ["a" =: "\xd7ff\&B"] ";a=%ED%9F%BF"
-    test "{;a:1}" ["a" =: "\x10000\&B"] ";a=%F0%90%80%80"
-    test "{;a:1}" ["a" =: emptyList] ""
-    test "{;a:1}" ["a" =: ["AB"]] ";a=AB"
-    test "{;a:1}" ["a" =: ["AB", "CD"]] ";a=AB,CD"
-    test "{;a:1}" ["a" =: emptyDictionary] ""
-    test "{;a:1}" ["a" =: ["AB" =: "12"]] ";a=AB,12"
-    test "{;a:1}" ["a" =: ["AB" =: "12", "CD" =: "34"]] ";a=AB,12,CD,34"
-    test "{;a,a}" [] ""
-    test "{;a,a}" ["a" =: emptyList] ""
-    test "{;a,a}" ["a" =: emptyDictionary] ""
-    test "{;a,a}" ["a" =: ""] ";a;a"
-    test "{;a,b}" ["a" =: ""] ";a"
-    test "{;a,b}" ["b" =: ""] ";b"
-    test "{;%aa}" ["%aa" =: "A"] ";%aa=A"
-    test "{;%aa}" ["%aa" =: ["A", "B"]] ";%aa=A,B"
-    test "{;%aa}" ["%aa" =: ["A" =: "1", "B" =: "2"]] ";%aa=A,1,B,2"
-    test "{;%aa*}" ["%aa" =: "A"] ";%aa=A"
-    test "{;%aa*}" ["%aa" =: ["A", "B"]] ";%aa=A;%aa=B"
-    test "{;%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] ";A=1;B=2"
-
-  Test.it "handles query expansion" $ do
-    test "{?a}" [] ""
-    test "{?a}" ["a" =: emptyList] ""
-    test "{?a}" ["a" =: emptyDictionary] ""
-    test "{?a}" ["a" =: ""] "?a="
-    test "{?a}" ["a" =: "A"] "?a=A"
-    test "{?a}" ["a" =: "~"] "?a=~"
-    test "{?a}" ["a" =: "%"] "?a=%25"
-    test "{?a}" ["a" =: "?"] "?a=%3F"
-    test "{?a}" ["a" =: "&"] "?a=%26"
-    test "{?a}" ["a" =: "\xa0"] "?a=%C2%A0"
-    test "{?a}" ["a" =: "\xd7ff"] "?a=%ED%9F%BF"
-    test "{?a}" ["a" =: "\x10000"] "?a=%F0%90%80%80"
-    test "{?a}" ["a" =: ["A"]] "?a=A"
-    test "{?a}" ["a" =: ["A", "B"]] "?a=A,B"
-    test "{?a}" ["a" =: ["%"]] "?a=%25"
-    test "{?a}" ["a" =: ["\xa0"]] "?a=%C2%A0"
-    test "{?a}" ["a" =: ["A" =: "1"]] "?a=A,1"
-    test "{?a}" ["a" =: ["A" =: "1", "B" =: "2"]] "?a=A,1,B,2"
-    test "{?a}" ["a" =: ["A" =: "%"]] "?a=A,%25"
-    test "{?a}" ["a" =: ["A" =: "\xa0"]] "?a=A,%C2%A0"
-    test "{?a}" ["a" =: ["%" =: "1"]] "?a=%25,1"
-    test "{?a*}" [] ""
-    test "{?a*}" ["a" =: ""] "?a="
-    test "{?a*}" ["a" =: "A"] "?a=A"
-    test "{?a*}" ["a" =: emptyList] ""
-    test "{?a*}" ["a" =: ["A"]] "?a=A"
-    test "{?a*}" ["a" =: ["A", "B"]] "?a=A&a=B"
-    test "{?a*}" ["a" =: emptyDictionary] ""
-    test "{?a*}" ["a" =: ["A" =: "1"]] "?A=1"
-    test "{?a*}" ["a" =: ["A" =: "1", "B" =: "2"]] "?A=1&B=2"
-    test "{?a:1}" [] ""
-    test "{?a:1}" ["a" =: ""] "?a="
-    test "{?a:1}" ["a" =: "A"] "?a=A"
-    test "{?a:1}" ["a" =: "AB"] "?a=A"
-    test "{?a:1}" ["a" =: "%B"] "?a=%25"
-    test "{?a:1}" ["a" =: "\xa0\&B"] "?a=%C2%A0"
-    test "{?a:1}" ["a" =: "\xd7ff\&B"] "?a=%ED%9F%BF"
-    test "{?a:1}" ["a" =: "\x10000\&B"] "?a=%F0%90%80%80"
-    test "{?a:1}" ["a" =: emptyList] ""
-    test "{?a:1}" ["a" =: ["AB"]] "?a=AB"
-    test "{?a:1}" ["a" =: ["AB", "CD"]] "?a=AB,CD"
-    test "{?a:1}" ["a" =: emptyDictionary] ""
-    test "{?a:1}" ["a" =: ["AB" =: "12"]] "?a=AB,12"
-    test "{?a:1}" ["a" =: ["AB" =: "12", "CD" =: "34"]] "?a=AB,12,CD,34"
-    test "{?a,a}" [] ""
-    test "{?a,a}" ["a" =: emptyList] ""
-    test "{?a,a}" ["a" =: emptyDictionary] ""
-    test "{?a,a}" ["a" =: ""] "?a=&a="
-    test "{?a,b}" ["a" =: ""] "?a="
-    test "{?a,b}" ["b" =: ""] "?b="
-    test "{?%aa}" ["%aa" =: "A"] "?%aa=A"
-    test "{?%aa}" ["%aa" =: ["A", "B"]] "?%aa=A,B"
-    test "{?%aa}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "?%aa=A,1,B,2"
-    test "{?%aa*}" ["%aa" =: "A"] "?%aa=A"
-    test "{?%aa*}" ["%aa" =: ["A", "B"]] "?%aa=A&%aa=B"
-    test "{?%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "?A=1&B=2"
-
-  Test.it "handles continuation expansion" $ do
-    test "{&a}" [] ""
-    test "{&a}" ["a" =: emptyList] ""
-    test "{&a}" ["a" =: emptyDictionary] ""
-    test "{&a}" ["a" =: ""] "&a="
-    test "{&a}" ["a" =: "A"] "&a=A"
-    test "{&a}" ["a" =: "~"] "&a=~"
-    test "{&a}" ["a" =: "%"] "&a=%25"
-    test "{&a}" ["a" =: "?"] "&a=%3F"
-    test "{&a}" ["a" =: "&"] "&a=%26"
-    test "{&a}" ["a" =: "\xa0"] "&a=%C2%A0"
-    test "{&a}" ["a" =: "\xd7ff"] "&a=%ED%9F%BF"
-    test "{&a}" ["a" =: "\x10000"] "&a=%F0%90%80%80"
-    test "{&a}" ["a" =: ["A"]] "&a=A"
-    test "{&a}" ["a" =: ["A", "B"]] "&a=A,B"
-    test "{&a}" ["a" =: ["%"]] "&a=%25"
-    test "{&a}" ["a" =: ["\xa0"]] "&a=%C2%A0"
-    test "{&a}" ["a" =: ["A" =: "1"]] "&a=A,1"
-    test "{&a}" ["a" =: ["A" =: "1", "B" =: "2"]] "&a=A,1,B,2"
-    test "{&a}" ["a" =: ["A" =: "%"]] "&a=A,%25"
-    test "{&a}" ["a" =: ["A" =: "\xa0"]] "&a=A,%C2%A0"
-    test "{&a}" ["a" =: ["%" =: "1"]] "&a=%25,1"
-    test "{&a*}" [] ""
-    test "{&a*}" ["a" =: ""] "&a="
-    test "{&a*}" ["a" =: "A"] "&a=A"
-    test "{&a*}" ["a" =: emptyList] ""
-    test "{&a*}" ["a" =: ["A"]] "&a=A"
-    test "{&a*}" ["a" =: ["A", "B"]] "&a=A&a=B"
-    test "{&a*}" ["a" =: emptyDictionary] ""
-    test "{&a*}" ["a" =: ["A" =: "1"]] "&A=1"
-    test "{&a*}" ["a" =: ["A" =: "1", "B" =: "2"]] "&A=1&B=2"
-    test "{&a:1}" [] ""
-    test "{&a:1}" ["a" =: ""] "&a="
-    test "{&a:1}" ["a" =: "A"] "&a=A"
-    test "{&a:1}" ["a" =: "AB"] "&a=A"
-    test "{&a:1}" ["a" =: "%B"] "&a=%25"
-    test "{&a:1}" ["a" =: "\xa0\&B"] "&a=%C2%A0"
-    test "{&a:1}" ["a" =: "\xd7ff\&B"] "&a=%ED%9F%BF"
-    test "{&a:1}" ["a" =: "\x10000\&B"] "&a=%F0%90%80%80"
-    test "{&a:1}" ["a" =: emptyList] ""
-    test "{&a:1}" ["a" =: ["AB"]] "&a=AB"
-    test "{&a:1}" ["a" =: ["AB", "CD"]] "&a=AB,CD"
-    test "{&a:1}" ["a" =: emptyDictionary] ""
-    test "{&a:1}" ["a" =: ["AB" =: "12"]] "&a=AB,12"
-    test "{&a:1}" ["a" =: ["AB" =: "12", "CD" =: "34"]] "&a=AB,12,CD,34"
-    test "{&a,a}" [] ""
-    test "{&a,a}" ["a" =: emptyList] ""
-    test "{&a,a}" ["a" =: emptyDictionary] ""
-    test "{&a,a}" ["a" =: ""] "&a=&a="
-    test "{&a,b}" ["a" =: ""] "&a="
-    test "{&a,b}" ["b" =: ""] "&b="
-    test "{&%aa}" ["%aa" =: "A"] "&%aa=A"
-    test "{&%aa}" ["%aa" =: ["A", "B"]] "&%aa=A,B"
-    test "{&%aa}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "&%aa=A,1,B,2"
-    test "{&%aa*}" ["%aa" =: "A"] "&%aa=A"
-    test "{&%aa*}" ["%aa" =: ["A", "B"]] "&%aa=A&%aa=B"
-    test "{&%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "&A=1&B=2"
-
-  Test.describe "supports quasi quotes" $ do
-
-    Test.it "as expressions" $ do
-      let qq x y = Just x `Test.shouldBe` Burrito.parse y
-      qq [Burrito.uriTemplate||] ""
-      qq [Burrito.uriTemplate|a|] "a"
-      qq [Burrito.uriTemplate|%00|] "%00"
-      qq [Burrito.uriTemplate|{a}|] "{a}"
-      qq [Burrito.uriTemplate|{a,b}|] "{a,b}"
-      qq [Burrito.uriTemplate|{a}{b}|] "{a}{b}"
-      qq [Burrito.uriTemplate|{a*}|] "{a*}"
-      qq [Burrito.uriTemplate|{a:1}|] "{a:1}"
-      qq [Burrito.uriTemplate|{+a}|] "{+a}"
-      qq [Burrito.uriTemplate|{#a}|] "{#a}"
-      qq [Burrito.uriTemplate|{.a}|] "{.a}"
-      qq [Burrito.uriTemplate|{/a}|] "{/a}"
-      qq [Burrito.uriTemplate|{;a}|] "{;a}"
-      qq [Burrito.uriTemplate|{?a}|] "{?a}"
-      qq [Burrito.uriTemplate|{&a}|] "{&a}"
-
-  Test.it "round trips" . QC.property $ \template ->
-    let rendered = Burrito.render $ unwrapTemplate template
-    in fmap Burrito.render (Burrito.parse rendered) == Just rendered
-
-test :: Stack.HasCallStack => String -> Values -> Expected -> IO ()
-test input values output =
-  fmap (Burrito.expand (fmap (fmap unwrapValue) values)) (Burrito.parse input)
-    `Test.shouldBe` expectedToMaybe output
-
-data Expected
-  = Failure
-  | Success String
-  deriving (Eq, Show)
-
-instance String.IsString Expected where
-  fromString = Success
-
-expectedToMaybe :: Expected -> Maybe String
-expectedToMaybe expected = case expected of
-  Failure -> Nothing
-  Success string -> Just string
-
-type Values = [(String, Value)]
-
-newtype Value = Value
-  { unwrapValue :: Burrito.Value
-  } deriving (Eq, Show)
-
-instance Exts.IsList Value where
-  type Item Value = Item
-  fromList items =
-    Value $ case Either.partitionEithers $ fmap unwrapItem items of
-      (strings, []) -> Burrito.listValue strings
-      ([], tuples) -> Burrito.dictionaryValue tuples
-      (_, _) -> error $ "fromList " <> show items <> " :: Value"
-  toList value = error $ "toList " <> show value
-
-instance String.IsString Value where
-  fromString = Value . Burrito.stringValue
-
-emptyList :: Value
-emptyList = Value $ Burrito.listValue []
-
-emptyDictionary :: Value
-emptyDictionary = Value $ Burrito.dictionaryValue []
-
-newtype Item = Item
-  { unwrapItem :: Either String (String, String)
-  } deriving (Eq, Show)
-
-instance String.IsString Item where
-  fromString = Item . Left
-
-class Pair a where
-  type K a
-  type V a
-  (=:) :: K a -> V a -> a
-
-instance Pair (a, b) where
-  type K (a, b) = a
-  type V (a, b) = b
-  (=:) = (,)
-
-instance Pair Item where
-  type K Item = String
-  type V Item = String
-  key =: value = Item $ Right (key, value)
-
-newtype Template = Template
-  { unwrapTemplate :: Burrito.Template
-  } deriving (Eq)
-
-instance Show Template where
-  show (Template t) = show (Burrito.render t, t)
-
-instance QC.Arbitrary Template where
-  arbitrary = Template <$> QC.arbitrary
-  shrink = fmap Template . QC.shrink . unwrapTemplate
-
-instance QC.Arbitrary Template.Template where
-  arbitrary = Template.Template <$> QC.arbitrary
-  shrink = fmap Template.Template . QC.shrink . Template.tokens
-
-instance QC.Arbitrary Token.Token where
-  arbitrary = QC.oneof
-    [Token.Expression <$> QC.arbitrary, Token.Literal <$> QC.arbitrary]
-  shrink token = case token of
-    Token.Expression expression -> Token.Expression <$> QC.shrink expression
-    Token.Literal literal -> Token.Literal <$> QC.shrink literal
-
-instance QC.Arbitrary Expression.Expression where
-  arbitrary = Expression.Expression <$> QC.arbitrary <*> QC.arbitrary
-  shrink expression = uncurry Expression.Expression <$> QC.shrink
-    (Expression.operator expression, Expression.variables expression)
-
-instance QC.Arbitrary Operator.Operator where
-  arbitrary = QC.elements
-    [ Operator.Ampersand
-    , Operator.FullStop
-    , Operator.None
-    , Operator.NumberSign
-    , Operator.PlusSign
-    , Operator.QuestionMark
-    , Operator.Semicolon
-    , Operator.Solidus
-    ]
-  shrink operator = case operator of
-    Operator.None -> []
-    _ -> [Operator.None]
-
-instance (QC.Arbitrary a) => QC.Arbitrary (NonEmpty.NonEmpty a) where
-  arbitrary = NonEmpty.NonEmpty <$> QC.arbitrary <*> QC.arbitrary
-  shrink nonEmpty = uncurry NonEmpty.NonEmpty
-    <$> QC.shrink (NonEmpty.first nonEmpty, NonEmpty.rest nonEmpty)
-
-instance QC.Arbitrary Variable.Variable where
-  arbitrary = Variable.Variable <$> QC.arbitrary <*> QC.arbitrary
-  shrink variable = uncurry Variable.Variable
-    <$> QC.shrink (Variable.modifier variable, Variable.name variable)
-
-instance QC.Arbitrary Modifier.Modifier where
-  arbitrary = QC.oneof
-    [ pure Modifier.Asterisk
-    , do
-      maxLength <- QC.choose (1, 9999)
-      maybe QC.discard pure $ Modifier.makeColon maxLength
-    , pure Modifier.None
-    ]
-  shrink modifier = case modifier of
-    Modifier.Asterisk -> [Modifier.None]
-    Modifier.Colon int -> Maybe.mapMaybe Modifier.makeColon $ QC.shrink int
-    Modifier.None -> []
-
-instance QC.Arbitrary Name.Name where
-  arbitrary = Name.Name <$> QC.arbitrary <*> QC.arbitrary
-  shrink name =
-    uncurry Name.Name <$> QC.shrink (Name.first name, Name.rest name)
-
-instance QC.Arbitrary VarChar.VarChar where
-  arbitrary = QC.oneof
-    [ do
-      hi <- QC.suchThat QC.arbitrary Char.isHexDigit
-      lo <- QC.suchThat QC.arbitrary Char.isHexDigit
-      maybe QC.discard pure $ VarChar.makeEncoded hi lo
-    , do
-      char <- QC.suchThat QC.arbitrary VarChar.isVarchar
-      maybe QC.discard pure $ VarChar.makeUnencoded char
-    ]
-  shrink varChar = case varChar of
-    VarChar.Encoded hi lo -> uncurry VarChar.Encoded <$> QC.shrink (hi, lo)
-    VarChar.Unencoded char ->
-      Maybe.mapMaybe VarChar.makeUnencoded $ QC.shrink char
-
-instance QC.Arbitrary Literal.Literal where
-  arbitrary = Literal.Literal <$> QC.arbitrary
-  shrink = fmap Literal.Literal . QC.shrink . Literal.characters
-
-instance QC.Arbitrary LitChar.LitChar where
-  arbitrary = QC.oneof
-    [ LitChar.Encoded <$> QC.arbitrary
-    , do
-      char <- QC.suchThat QC.arbitrary LitChar.isLiteral
-      maybe QC.discard pure $ LitChar.makeUnencoded char
-    ]
-  shrink character = case character of
-    LitChar.Encoded word8 -> LitChar.Encoded <$> QC.shrink word8
-    LitChar.Unencoded char ->
-      Maybe.mapMaybe LitChar.makeUnencoded $ QC.shrink char
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+module Main
+  ( main
+  )
+where
+
+import qualified Burrito
+import qualified Burrito.Internal.Parse as Parse
+import qualified Burrito.Internal.Render as Render
+import qualified Burrito.Internal.Type.Case as Case
+import qualified Burrito.Internal.Type.Character as Character
+import qualified Burrito.Internal.Type.Digit as Digit
+import qualified Burrito.Internal.Type.Expression as Expression
+import qualified Burrito.Internal.Type.Field as Field
+import qualified Burrito.Internal.Type.Literal as Literal
+import qualified Burrito.Internal.Type.MaxLength as MaxLength
+import qualified Burrito.Internal.Type.Modifier as Modifier
+import qualified Burrito.Internal.Type.Name as Name
+import qualified Burrito.Internal.Type.Operator as Operator
+import qualified Burrito.Internal.Type.Template as Template
+import qualified Burrito.Internal.Type.Token as Token
+import qualified Burrito.Internal.Type.Value as Value
+import qualified Burrito.Internal.Type.Variable as Variable
+import qualified Control.Monad as Monad
+import qualified Data.List as List
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map as Map
+import qualified Data.Maybe as Maybe
+import qualified Data.Set as Set
+import qualified Data.String as String
+import qualified Data.Text as Text
+import qualified GHC.Stack as Stack
+import qualified Test.Hspec as Hspec
+import qualified Test.Hspec.QuickCheck as Hspec
+import qualified Test.QuickCheck as QC
+
+main :: IO ()
+main = Hspec.hspec . Hspec.describe "Burrito" $ do
+
+  Monad.forM_ tests $ \test ->
+    Hspec.it (show (testInput test, unwrapOutput $ testOutput test))
+      $ runTest test
+
+  Hspec.describe "match" $ do
+    let
+      matchTest
+        :: Stack.HasCallStack
+        => String
+        -> [(String, Burrito.Value)]
+        -> String
+        -> Hspec.Spec
+      matchTest input values output = Hspec.it (show (input, output)) $ do
+        template <- maybe (fail "invalid Template") pure $ Burrito.parse input
+        let matches = Burrito.match output template
+        matches `Hspec.shouldSatisfy` elem values
+        Monad.forM_ matches $ \match -> do
+          let it = Burrito.expand match template
+          Monad.when (it /= output) . fail $ show
+            (input, values, output, matches, match, it)
+
+    matchTest "" [] ""
+    matchTest "a" [] "a"
+    matchTest "!" [] "!"
+    matchTest "\xa0" [] "%C2%A0"
+
+    matchTest "{a}" ["a" =: s ""] ""
+    matchTest "{a}" ["a" =: s "A"] "A"
+    matchTest "{a}" ["a" =: s "AB"] "AB"
+
+    matchTest "a{b}" ["b" =: s ""] "a"
+    matchTest "a{b}" ["b" =: s "B"] "aB"
+    matchTest "a{b}" ["b" =: s "BC"] "aBC"
+
+    matchTest "{a}b" ["a" =: s ""] "b"
+    matchTest "{a}b" ["a" =: s "A"] "Ab"
+    matchTest "{a}b" ["a" =: s "AB"] "ABb"
+
+    matchTest "{a}{a}" ["a" =: s "A"] "AA"
+    matchTest "{a}{a}" ["a" =: s "AB"] "ABAB"
+
+    matchTest "{a}" ["a" =: s "%"] "%25"
+    matchTest "{a}" ["a" =: s "/"] "%2F"
+    matchTest "{a}" ["a" =: s "\xa0"] "%C2%A0"
+    matchTest "{a}" ["a" =: s "\xd7ff"] "%ED%9F%BF"
+    matchTest "{a}" ["a" =: s "\x10000"] "%F0%90%80%80"
+    matchTest "{a}" ["a" =: s "A/B"] "A%2FB"
+    matchTest "{a}" ["a" =: s "WX\xa0\xa1YZ"] "WX%C2%A0%C2%A1YZ"
+
+    matchTest "{+a}" ["a" =: s "%"] "%25"
+    matchTest "{+a}" ["a" =: s "/"] "/"
+
+    matchTest "{#a}" [] ""
+    matchTest "{#a}" ["a" =: s ""] "#"
+    matchTest "{#a}" ["a" =: s "A"] "#A"
+    matchTest "{#a}" ["a" =: s "%"] "#%25"
+    matchTest "{#a}" ["a" =: s "/"] "#/"
+
+    matchTest "{.a}" [] ""
+    matchTest "{.a}" ["a" =: s ""] "."
+    matchTest "{.a}" ["a" =: s "A"] ".A"
+    matchTest "{.a}" ["a" =: s "%"] ".%25"
+    matchTest "{.a}" ["a" =: s "/"] ".%2F"
+
+    matchTest "{/a}" [] ""
+    matchTest "{/a}" ["a" =: s ""] "/"
+    matchTest "{/a}" ["a" =: s "A"] "/A"
+    matchTest "{/a}" ["a" =: s "%"] "/%25"
+    matchTest "{/a}" ["a" =: s "/"] "/%2F"
+
+    matchTest "{;a}" [] ""
+    matchTest "{;a}" ["a" =: s ""] ";a"
+    matchTest "{;a}" ["a" =: s "A"] ";a=A"
+    matchTest "{;a}" ["a" =: s "%"] ";a=%25"
+    matchTest "{;a}" ["a" =: s "/"] ";a=%2F"
+
+    matchTest "{?a}" [] ""
+    matchTest "{?a}" ["a" =: s ""] "?a="
+    matchTest "{?a}" ["a" =: s "A"] "?a=A"
+    matchTest "{?a}" ["a" =: s "%"] "?a=%25"
+    matchTest "{?a}" ["a" =: s "/"] "?a=%2F"
+
+    matchTest "{&a}" [] ""
+    matchTest "{&a}" ["a" =: s ""] "&a="
+    matchTest "{&a}" ["a" =: s "A"] "&a=A"
+    matchTest "{&a}" ["a" =: s "%"] "&a=%25"
+    matchTest "{&a}" ["a" =: s "/"] "&a=%2F"
+
+    matchTest "{a,b}" ["a" =: s "A", "b" =: s "B"] "A,B"
+    matchTest "{+a,b}" ["a" =: s "A", "b" =: s "B"] "A,B"
+    matchTest "{#a,b}" ["a" =: s "A", "b" =: s "B"] "#A,B"
+    matchTest "{.a,b}" ["a" =: s "A", "b" =: s "B"] ".A.B"
+    matchTest "{/a,b}" ["a" =: s "A", "b" =: s "B"] "/A/B"
+    matchTest "{;a,b}" ["a" =: s "A", "b" =: s "B"] ";a=A;b=B"
+    matchTest "{?a,b}" ["a" =: s "A", "b" =: s "B"] "?a=A&b=B"
+    matchTest "{&a,b}" ["a" =: s "A", "b" =: s "B"] "&a=A&b=B"
+
+    matchTest "{a,b}" ["a" =: s "A"] "A"
+    matchTest "{a,b,c}" ["a" =: s "A"] "A"
+    matchTest "{a,b,c}" ["a" =: s "A", "b" =: s "B"] "A,B"
+    matchTest "{a}{a,b}" ["b" =: s "B"] "B"
+    matchTest "{a}{a,b,c}" ["b" =: s "B"] "B"
+    matchTest "{a,b}{a,b,c}" ["c" =: s "C"] "C"
+    matchTest "{b}{a,b,c}" ["a" =: s "A", "c" =: s "C"] "A,C"
+    matchTest "{a}{a,b,c}" ["b" =: s "B", "c" =: s "C"] "B,C"
+
+    matchTest "{a:1}/{a}" ["a" =: s "AB"] "A/AB"
+
+    -- TODO: Test matching on explode modifier.
+    -- matchTest "{a*}" ["a" =: s "A"] "A"
+
+    -- TODO: Test matching on lists.
+    -- matchTest "{a}" ["a" =: l ["A", "B"]] "A,B"
+
+    -- TODO: Test matching on dictionaries.
+    -- matchTest "{a}" ["a" =: d ["k" =: "v"]] "k,v"
+
+  Hspec.describe "uriTemplate"
+    . Hspec.it "works as an expression"
+    $ Just [Burrito.uriTemplate|a{b}c|]
+    `Hspec.shouldBe` Burrito.parse "a{b}c"
+
+  Hspec.modifyMaxSize (const 10)
+    . Hspec.it "round trips"
+    . QC.property
+    $ \(Template template) ->
+        Burrito.parse (Burrito.render template) == Just template
+
+-- brittany-next-binding --columns 160
+tests :: [Test]
+tests = mconcat
+  [ [ Test "" [] ""
+    , Test "" ["a" =: s ""] ""
+    , Test "" ["a" =: l []] ""
+    , Test "" ["a" =: d []] ""
+    , Test "!" [] "!"
+    , Test "#" [] "#"
+    , Test "$" [] "$"
+    , Test "&" [] "&"
+    , Test "(" [] "("
+    , Test ")" [] ")"
+    , Test "*" [] "*"
+    , Test "+" [] "+"
+    , Test "," [] ","
+    , Test "-" [] "-"
+    , Test "." [] "."
+    , Test "/" [] "/"
+    , Test "0" [] "0"
+    , Test "9" [] "9"
+    , Test ":" [] ":"
+    , Test ";" [] ";"
+    , Test "=" [] "="
+    , Test "?" [] "?"
+    , Test "@" [] "@"
+    , Test "A" [] "A"
+    , Test "Z" [] "Z"
+    , Test "[" [] "["
+    , Test "]" [] "]"
+    , Test "_" [] "_"
+    , Test "a" [] "a"
+    , Test "z" [] "z"
+    , Test "~" [] "~"
+    , Test "\xa0" [] "%C2%A0"
+    , Test "\xd7ff" [] "%ED%9F%BF"
+    , Test "\xf900" [] "%EF%A4%80"
+    , Test "\xfdcf" [] "%EF%B7%8F"
+    , Test "\xfdf0" [] "%EF%B7%B0"
+    , Test "\xffef" [] "%EF%BF%AF"
+    , Test "\x10000" [] "%F0%90%80%80"
+    , Test "\x1fffd" [] "%F0%9F%BF%BD"
+    , Test "\x20000" [] "%F0%A0%80%80"
+    , Test "\x2fffd" [] "%F0%AF%BF%BD"
+    , Test "\x30000" [] "%F0%B0%80%80"
+    , Test "\x3fffd" [] "%F0%BF%BF%BD"
+    , Test "\x40000" [] "%F1%80%80%80"
+    , Test "\x4fffd" [] "%F1%8F%BF%BD"
+    , Test "\x50000" [] "%F1%90%80%80"
+    , Test "\x5fffd" [] "%F1%9F%BF%BD"
+    , Test "\x60000" [] "%F1%A0%80%80"
+    , Test "\x6fffd" [] "%F1%AF%BF%BD"
+    , Test "\x70000" [] "%F1%B0%80%80"
+    , Test "\x7fffd" [] "%F1%BF%BF%BD"
+    , Test "\x80000" [] "%F2%80%80%80"
+    , Test "\x8fffd" [] "%F2%8F%BF%BD"
+    , Test "\x90000" [] "%F2%90%80%80"
+    , Test "\x9fffd" [] "%F2%9F%BF%BD"
+    , Test "\xa0000" [] "%F2%A0%80%80"
+    , Test "\xafffd" [] "%F2%AF%BF%BD"
+    , Test "\xb0000" [] "%F2%B0%80%80"
+    , Test "\xbfffd" [] "%F2%BF%BF%BD"
+    , Test "\xc0000" [] "%F3%80%80%80"
+    , Test "\xcfffd" [] "%F3%8F%BF%BD"
+    , Test "\xd0000" [] "%F3%90%80%80"
+    , Test "\xdfffd" [] "%F3%9F%BF%BD"
+    , Test "\xe1000" [] "%F3%A1%80%80"
+    , Test "\xefffd" [] "%F3%AF%BF%BD"
+    , Test "\xe000" [] "%EE%80%80"
+    , Test "\xf8ff" [] "%EF%A3%BF"
+    , Test "\xf0000" [] "%F3%B0%80%80"
+    , Test "\xffffd" [] "%F3%BF%BF%BD"
+    , Test "\x100000" [] "%F4%80%80%80"
+    , Test "\x10fffd" [] "%F4%8F%BF%BD"
+    , Test "%00" [] "%00"
+    , Test "%AA" [] "%AA"
+    , Test "%Aa" [] "%Aa"
+    , Test "%aA" [] "%aA"
+    , Test "%aa" [] "%aa"
+    , Test "%" [] noParse
+    , Test "%0" [] noParse
+    , Test "%0z" [] noParse
+    , Test "%z" [] noParse
+    , Test "%30" [] "%30"
+    , Test " " [] noParse
+    , Test "\"" [] noParse
+    , Test "'" [] noParse
+    , Test "%" [] noParse
+    , Test "<" [] noParse
+    , Test ">" [] noParse
+    , Test "\\" [] noParse
+    , Test "^" [] noParse
+    , Test "`" [] noParse
+    , Test "{" [] noParse
+    , Test "|" [] noParse
+    , Test "}" [] noParse
+    , Test "{}" [] noParse
+    , Test "{,}" [] noParse
+    , Test "{a,,b}" [] noParse
+    , Test "{+}" [] noParse
+    , Test "{:1}" [] noParse
+    , Test "{*}" [] noParse
+    , Test "{AZ}" [] ""
+    , Test "{az}" [] ""
+    , Test "{09}" [] ""
+    , Test "{_a}" [] ""
+    , Test "{a_}" [] ""
+    , Test "{_}" [] ""
+    , Test "{A.A}" [] ""
+    , Test "{a.a}" [] ""
+    , Test "{0.0}" [] ""
+    , Test "{_._}" [] ""
+    , Test "{%aa.%aa}" [] ""
+    , Test "{.}" [] noParse
+    , Test "{a.}" [] noParse
+    , Test "{+.a}" [] noParse
+    , Test "{a..b}" [] noParse
+    , Test "{%00}" [] ""
+    , Test "{%}" [] noParse
+    , Test "{%0}" [] noParse
+    , Test "{%0z}" [] noParse
+    , Test "{%z}" [] noParse
+    , Test "{!}" [] noParse
+    , Test "{" [] noParse
+    , Test "{{}" [] noParse
+    , Test "}" [] noParse
+    , Test "{}}" [] noParse
+    , Test "{a,b}" [] ""
+    , Test "{a,b,c,d}" [] ""
+    , Test "{a,a}" [] ""
+    , Test "{a:5}" [] ""
+    , Test "{a:67}" [] ""
+    , Test "{a:801}" [] ""
+    , Test "{a:234}" [] ""
+    , Test "{a:9999}" [] ""
+    , Test "{a:123}" ["a" =: s (replicate 200 'a')] . Output . Just $ replicate 123 'a'
+    , Test "{a:}" [] noParse
+    , Test "{a:0}" [] noParse
+    , Test "{a:10000}" [] noParse
+    , Test "{a:-1}" [] noParse
+    , Test "{a*}" [] ""
+    , Test "{a:1*}" [] noParse
+    , Test "{a*:1}" [] noParse
+    , Test "{a,b:1,c*}" [] ""
+    , Test "{+a}" [] ""
+    , Test "{#a}" [] ""
+    , Test "{.a}" [] ""
+    , Test "{/a}" [] ""
+    , Test "{;a}" [] ""
+    , Test "{?a}" [] ""
+    , Test "{&a}" [] ""
+    , Test "{=a}" [] noParse
+    , Test "{,a}" [] noParse
+    , Test "{!a}" [] noParse
+    , Test "{@a}" [] noParse
+    , Test "{|a}" [] noParse
+    , Test "{+#a}" [] noParse
+    , Test "{+a,#b}" [] noParse
+    , Test "{+a:1}" [] ""
+    , Test "{#a*}" [] ""
+    , Test "{+a,b}" [] ""
+    , Test "{#a,b}" [] ""
+    , Test "{.a,b}" [] ""
+    , Test "{/a,b}" [] ""
+    , Test "{;a,b}" [] ""
+    , Test "{?a,b}" [] ""
+    , Test "{&a,b}" [] ""
+    , Test "{a}{b}" [] ""
+    , Test "{a}{b}{c}{d}" [] ""
+    , Test "{a}{a}" [] ""
+    , Test "{{}}" [] noParse
+    , Test "{a{b}}" [] noParse
+    , Test "{{a}b}" [] noParse
+    , Test "{a{b}c}" [] noParse
+    , Test "a{b}" [] "a"
+    , Test "{a}b" [] "b"
+    , Test "a{b}c" [] "ac"
+    , Test "{a}b{c}" [] "b"
+    , Test "{a}" [] ""
+    , Test "{+a}" [] ""
+    , Test "{#a}" [] ""
+    , Test "{.a}" [] ""
+    , Test "{/a}" [] ""
+    , Test "{;a}" [] ""
+    , Test "{?a}" [] ""
+    , Test "{&a}" [] ""
+    , Test "http://example.com/~{username}/" ["username" =: s "fred"] "http://example.com/~fred/"
+    , Test "http://example.com/~{username}/" ["username" =: s "mark"] "http://example.com/~mark/"
+    , Test "http://example.com/dictionary/{term:1}/{term}" ["term" =: s "cat"] "http://example.com/dictionary/c/cat"
+    , Test "http://example.com/dictionary/{term:1}/{term}" ["term" =: s "dog"] "http://example.com/dictionary/d/dog"
+    , Test "http://example.com/search{?q,lang}" ["q" =: s "cat", "lang" =: s "en"] "http://example.com/search?q=cat&lang=en"
+    , Test "http://example.com/search{?q,lang}" ["q" =: s "chien", "lang" =: s "fr"] "http://example.com/search?q=chien&lang=fr"
+    , Test "http://www.example.com/foo{?query,number}" ["query" =: s "mycelium", "number" =: s "100"] "http://www.example.com/foo?query=mycelium&number=100"
+    , Test "http://www.example.com/foo{?query,number}" ["number" =: s "100"] "http://www.example.com/foo?number=100"
+    , Test "http://www.example.com/foo{?query,number}" [] "http://www.example.com/foo"
+    , Test "{a}" [] ""
+    , Test "{a}" ["a" =: l []] ""
+    , Test "{a}" ["a" =: d []] ""
+    , Test "{a}" ["a" =: s ""] ""
+    , Test "{a}" ["a" =: s "A"] "A"
+    , Test "{a}" ["a" =: s "~"] "~"
+    , Test "{a}" ["a" =: s "%"] "%25"
+    , Test "{a}" ["a" =: s "?"] "%3F"
+    , Test "{a}" ["a" =: s "&"] "%26"
+    , Test "{a}" ["a" =: s "\xa0"] "%C2%A0"
+    , Test "{a}" ["a" =: s "\xd7ff"] "%ED%9F%BF"
+    , Test "{a}" ["a" =: s "\x10000"] "%F0%90%80%80"
+    , Test "{a}" ["a" =: l ["A"]] "A"
+    , Test "{a}" ["a" =: l ["A", "B"]] "A,B"
+    , Test "{a}" ["a" =: l ["%"]] "%25"
+    , Test "{a}" ["a" =: l ["\xa0"]] "%C2%A0"
+    , Test "{a}" ["a" =: d ["A" =: "1"]] "A,1"
+    , Test "{a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "A,1,B,2"
+    , Test "{a}" ["a" =: d ["A" =: "%"]] "A,%25"
+    , Test "{a}" ["a" =: d ["A" =: "\xa0"]] "A,%C2%A0"
+    , Test "{a}" ["a" =: d ["%" =: "1"]] "%25,1"
+    , Test "{a*}" [] ""
+    , Test "{a*}" ["a" =: s ""] ""
+    , Test "{a*}" ["a" =: s "A"] "A"
+    , Test "{a*}" ["a" =: l []] ""
+    , Test "{a*}" ["a" =: l ["A"]] "A"
+    , Test "{a*}" ["a" =: l ["A", "B"]] "A,B"
+    , Test "{a*}" ["a" =: d []] ""
+    , Test "{a*}" ["a" =: d ["A" =: "1"]] "A=1"
+    , Test "{a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "A=1,B=2"
+    , Test "{a:1}" [] ""
+    , Test "{a:1}" ["a" =: s ""] ""
+    , Test "{a:1}" ["a" =: s "A"] "A"
+    , Test "{a:1}" ["a" =: s "AB"] "A"
+    , Test "{a:1}" ["a" =: s "%B"] "%25"
+    , Test "{a:1}" ["a" =: s "\xa0\&B"] "%C2%A0"
+    , Test "{a:1}" ["a" =: s "\xd7ff\&B"] "%ED%9F%BF"
+    , Test "{a:1}" ["a" =: s "\x10000\&B"] "%F0%90%80%80"
+    , Test "{a:1}" ["a" =: l []] ""
+    , Test "{a:1}" ["a" =: l ["AB"]] "AB"
+    , Test "{a:1}" ["a" =: l ["AB", "CD"]] "AB,CD"
+    , Test "{a:1}" ["a" =: d []] ""
+    , Test "{a:1}" ["a" =: d ["AB" =: "12"]] "AB,12"
+    , Test "{a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "AB,12,CD,34"
+    , Test "{a,a}" [] ""
+    , Test "{a,a}" ["a" =: l []] ""
+    , Test "{a,a}" ["a" =: d []] ""
+    , Test "{a,a}" ["a" =: s ""] ","
+    , Test "{a,b}" ["a" =: s ""] ""
+    , Test "{a,b}" ["b" =: s ""] ""
+    , Test "{%aa}" ["%aa" =: s "A"] "A"
+    , Test "{%aa}" ["%aa" =: l ["A", "B"]] "A,B"
+    , Test "{%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "A,1,B,2"
+    , Test "{%aa*}" ["%aa" =: s "A"] "A"
+    , Test "{%aa*}" ["%aa" =: l ["A", "B"]] "A,B"
+    , Test "{%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "A=1,B=2"
+    , Test "{+a}" [] ""
+    , Test "{+a}" ["a" =: l []] ""
+    , Test "{+a}" ["a" =: d []] ""
+    , Test "{+a}" ["a" =: s ""] ""
+    , Test "{+a}" ["a" =: s "A"] "A"
+    , Test "{+a}" ["a" =: s "~"] "~"
+    , Test "{+a}" ["a" =: s "%"] "%25"
+    , Test "{+a}" ["a" =: s "?"] "?"
+    , Test "{+a}" ["a" =: s "&"] "&"
+    , Test "{+a}" ["a" =: s "\xa0"] "%C2%A0"
+    , Test "{+a}" ["a" =: s "\xd7ff"] "%ED%9F%BF"
+    , Test "{+a}" ["a" =: s "\x10000"] "%F0%90%80%80"
+    , Test "{+a}" ["a" =: l ["A"]] "A"
+    , Test "{+a}" ["a" =: l ["A", "B"]] "A,B"
+    , Test "{+a}" ["a" =: l ["%"]] "%25"
+    , Test "{+a}" ["a" =: l ["\xa0"]] "%C2%A0"
+    , Test "{+a}" ["a" =: d ["A" =: "1"]] "A,1"
+    , Test "{+a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "A,1,B,2"
+    , Test "{+a}" ["a" =: d ["A" =: "%"]] "A,%25"
+    , Test "{+a}" ["a" =: d ["A" =: "\xa0"]] "A,%C2%A0"
+    , Test "{+a}" ["a" =: d ["%" =: "1"]] "%25,1"
+    , Test "{+a*}" [] ""
+    , Test "{+a*}" ["a" =: s ""] ""
+    , Test "{+a*}" ["a" =: s "A"] "A"
+    , Test "{+a*}" ["a" =: l []] ""
+    , Test "{+a*}" ["a" =: l ["A"]] "A"
+    , Test "{+a*}" ["a" =: l ["A", "B"]] "A,B"
+    , Test "{+a*}" ["a" =: d []] ""
+    , Test "{+a*}" ["a" =: d ["A" =: "1"]] "A=1"
+    , Test "{+a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "A=1,B=2"
+    , Test "{+a:1}" [] ""
+    , Test "{+a:1}" ["a" =: s ""] ""
+    , Test "{+a:1}" ["a" =: s "A"] "A"
+    , Test "{+a:1}" ["a" =: s "AB"] "A"
+    , Test "{+a:1}" ["a" =: s "%B"] "%25"
+    , Test "{+a:1}" ["a" =: s "\xa0\&B"] "%C2%A0"
+    , Test "{+a:1}" ["a" =: s "\xd7ff\&B"] "%ED%9F%BF"
+    , Test "{+a:1}" ["a" =: s "\x10000\&B"] "%F0%90%80%80"
+    , Test "{+a:1}" ["a" =: l []] ""
+    , Test "{+a:1}" ["a" =: l ["AB"]] "AB"
+    , Test "{+a:1}" ["a" =: l ["AB", "CD"]] "AB,CD"
+    , Test "{+a:1}" ["a" =: d []] ""
+    , Test "{+a:1}" ["a" =: d ["AB" =: "12"]] "AB,12"
+    , Test "{+a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "AB,12,CD,34"
+    , Test "{+a,a}" [] ""
+    , Test "{+a,a}" ["a" =: l []] ""
+    , Test "{+a,a}" ["a" =: d []] ""
+    , Test "{+a,a}" ["a" =: s ""] ","
+    , Test "{+a,b}" ["a" =: s ""] ""
+    , Test "{+a,b}" ["b" =: s ""] ""
+    , Test "{+%aa}" ["%aa" =: s "A"] "A"
+    , Test "{+%aa}" ["%aa" =: l ["A", "B"]] "A,B"
+    , Test "{+%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "A,1,B,2"
+    , Test "{+%aa*}" ["%aa" =: s "A"] "A"
+    , Test "{+%aa*}" ["%aa" =: l ["A", "B"]] "A,B"
+    , Test "{+%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "A=1,B=2"
+    , Test "{#a}" [] ""
+    , Test "{#a}" ["a" =: l []] ""
+    , Test "{#a}" ["a" =: d []] ""
+    , Test "{#a}" ["a" =: s ""] "#"
+    , Test "{#a}" ["a" =: s "A"] "#A"
+    , Test "{#a}" ["a" =: s "~"] "#~"
+    , Test "{#a}" ["a" =: s "%"] "#%25"
+    , Test "{#a}" ["a" =: s "?"] "#?"
+    , Test "{#a}" ["a" =: s "&"] "#&"
+    , Test "{#a}" ["a" =: s "\xa0"] "#%C2%A0"
+    , Test "{#a}" ["a" =: s "\xd7ff"] "#%ED%9F%BF"
+    , Test "{#a}" ["a" =: s "\x10000"] "#%F0%90%80%80"
+    , Test "{#a}" ["a" =: l ["A"]] "#A"
+    , Test "{#a}" ["a" =: l ["A", "B"]] "#A,B"
+    , Test "{#a}" ["a" =: l ["%"]] "#%25"
+    , Test "{#a}" ["a" =: l ["\xa0"]] "#%C2%A0"
+    , Test "{#a}" ["a" =: d ["A" =: "1"]] "#A,1"
+    , Test "{#a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "#A,1,B,2"
+    , Test "{#a}" ["a" =: d ["A" =: "%"]] "#A,%25"
+    , Test "{#a}" ["a" =: d ["A" =: "\xa0"]] "#A,%C2%A0"
+    , Test "{#a}" ["a" =: d ["%" =: "1"]] "#%25,1"
+    , Test "{#a*}" [] ""
+    , Test "{#a*}" ["a" =: s ""] "#"
+    , Test "{#a*}" ["a" =: s "A"] "#A"
+    , Test "{#a*}" ["a" =: l []] ""
+    , Test "{#a*}" ["a" =: l ["A"]] "#A"
+    , Test "{#a*}" ["a" =: l ["A", "B"]] "#A,B"
+    , Test "{#a*}" ["a" =: d []] ""
+    , Test "{#a*}" ["a" =: d ["A" =: "1"]] "#A=1"
+    , Test "{#a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "#A=1,B=2"
+    , Test "{#a:1}" [] ""
+    , Test "{#a:1}" ["a" =: s ""] "#"
+    , Test "{#a:1}" ["a" =: s "A"] "#A"
+    , Test "{#a:1}" ["a" =: s "AB"] "#A"
+    , Test "{#a:1}" ["a" =: s "%B"] "#%25"
+    , Test "{#a:1}" ["a" =: s "\xa0\&B"] "#%C2%A0"
+    , Test "{#a:1}" ["a" =: s "\xd7ff\&B"] "#%ED%9F%BF"
+    , Test "{#a:1}" ["a" =: s "\x10000\&B"] "#%F0%90%80%80"
+    , Test "{#a:1}" ["a" =: l []] ""
+    , Test "{#a:1}" ["a" =: l ["AB"]] "#AB"
+    , Test "{#a:1}" ["a" =: l ["AB", "CD"]] "#AB,CD"
+    , Test "{#a:1}" ["a" =: d []] ""
+    , Test "{#a:1}" ["a" =: d ["AB" =: "12"]] "#AB,12"
+    , Test "{#a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "#AB,12,CD,34"
+    , Test "{#a,a}" [] ""
+    , Test "{#a,a}" ["a" =: l []] ""
+    , Test "{#a,a}" ["a" =: d []] ""
+    , Test "{#a,a}" ["a" =: s ""] "#,"
+    , Test "{#a,b}" ["a" =: s ""] "#"
+    , Test "{#a,b}" ["b" =: s ""] "#"
+    , Test "{#%aa}" ["%aa" =: s "A"] "#A"
+    , Test "{#%aa}" ["%aa" =: l ["A", "B"]] "#A,B"
+    , Test "{#%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "#A,1,B,2"
+    , Test "{#%aa*}" ["%aa" =: s "A"] "#A"
+    , Test "{#%aa*}" ["%aa" =: l ["A", "B"]] "#A,B"
+    , Test "{#%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "#A=1,B=2"
+    , Test "{.a}" [] ""
+    , Test "{.a}" ["a" =: l []] ""
+    , Test "{.a}" ["a" =: d []] ""
+    , Test "{.a}" ["a" =: s ""] "."
+    , Test "{.a}" ["a" =: s "A"] ".A"
+    , Test "{.a}" ["a" =: s "~"] ".~"
+    , Test "{.a}" ["a" =: s "%"] ".%25"
+    , Test "{.a}" ["a" =: s "?"] ".%3F"
+    , Test "{.a}" ["a" =: s "&"] ".%26"
+    , Test "{.a}" ["a" =: s "\xa0"] ".%C2%A0"
+    , Test "{.a}" ["a" =: s "\xd7ff"] ".%ED%9F%BF"
+    , Test "{.a}" ["a" =: s "\x10000"] ".%F0%90%80%80"
+    , Test "{.a}" ["a" =: l ["A"]] ".A"
+    , Test "{.a}" ["a" =: l ["A", "B"]] ".A,B"
+    , Test "{.a}" ["a" =: l ["%"]] ".%25"
+    , Test "{.a}" ["a" =: l ["\xa0"]] ".%C2%A0"
+    , Test "{.a}" ["a" =: d ["A" =: "1"]] ".A,1"
+    , Test "{.a}" ["a" =: d ["A" =: "1", "B" =: "2"]] ".A,1,B,2"
+    , Test "{.a}" ["a" =: d ["A" =: "%"]] ".A,%25"
+    , Test "{.a}" ["a" =: d ["A" =: "\xa0"]] ".A,%C2%A0"
+    , Test "{.a}" ["a" =: d ["%" =: "1"]] ".%25,1"
+    , Test "{.a*}" [] ""
+    , Test "{.a*}" ["a" =: s ""] "."
+    , Test "{.a*}" ["a" =: s "A"] ".A"
+    , Test "{.a*}" ["a" =: l []] ""
+    , Test "{.a*}" ["a" =: l ["A"]] ".A"
+    , Test "{.a*}" ["a" =: l ["A", "B"]] ".A.B"
+    , Test "{.a*}" ["a" =: d []] ""
+    , Test "{.a*}" ["a" =: d ["A" =: "1"]] ".A=1"
+    , Test "{.a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] ".A=1.B=2"
+    , Test "{.a:1}" [] ""
+    , Test "{.a:1}" ["a" =: s ""] "."
+    , Test "{.a:1}" ["a" =: s "A"] ".A"
+    , Test "{.a:1}" ["a" =: s "AB"] ".A"
+    , Test "{.a:1}" ["a" =: s "%B"] ".%25"
+    , Test "{.a:1}" ["a" =: s "\xa0\&B"] ".%C2%A0"
+    , Test "{.a:1}" ["a" =: s "\xd7ff\&B"] ".%ED%9F%BF"
+    , Test "{.a:1}" ["a" =: s "\x10000\&B"] ".%F0%90%80%80"
+    , Test "{.a:1}" ["a" =: l []] ""
+    , Test "{.a:1}" ["a" =: l ["AB"]] ".AB"
+    , Test "{.a:1}" ["a" =: l ["AB", "CD"]] ".AB,CD"
+    , Test "{.a:1}" ["a" =: d []] ""
+    , Test "{.a:1}" ["a" =: d ["AB" =: "12"]] ".AB,12"
+    , Test "{.a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] ".AB,12,CD,34"
+    , Test "{.a,a}" [] ""
+    , Test "{.a,a}" ["a" =: l []] ""
+    , Test "{.a,a}" ["a" =: d []] ""
+    , Test "{.a,a}" ["a" =: s ""] ".."
+    , Test "{.a,b}" ["a" =: s ""] "."
+    , Test "{.a,b}" ["b" =: s ""] "."
+    , Test "{.%aa}" ["%aa" =: s "A"] ".A"
+    , Test "{.%aa}" ["%aa" =: l ["A", "B"]] ".A,B"
+    , Test "{.%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] ".A,1,B,2"
+    , Test "{.%aa*}" ["%aa" =: s "A"] ".A"
+    , Test "{.%aa*}" ["%aa" =: l ["A", "B"]] ".A.B"
+    , Test "{.%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] ".A=1.B=2"
+    , Test "{/a}" [] ""
+    , Test "{/a}" ["a" =: l []] ""
+    , Test "{/a}" ["a" =: d []] ""
+    , Test "{/a}" ["a" =: s ""] "/"
+    , Test "{/a}" ["a" =: s "A"] "/A"
+    , Test "{/a}" ["a" =: s "~"] "/~"
+    , Test "{/a}" ["a" =: s "%"] "/%25"
+    , Test "{/a}" ["a" =: s "?"] "/%3F"
+    , Test "{/a}" ["a" =: s "&"] "/%26"
+    , Test "{/a}" ["a" =: s "\xa0"] "/%C2%A0"
+    , Test "{/a}" ["a" =: s "\xd7ff"] "/%ED%9F%BF"
+    , Test "{/a}" ["a" =: s "\x10000"] "/%F0%90%80%80"
+    , Test "{/a}" ["a" =: l ["A"]] "/A"
+    , Test "{/a}" ["a" =: l ["A", "B"]] "/A,B"
+    , Test "{/a}" ["a" =: l ["%"]] "/%25"
+    , Test "{/a}" ["a" =: l ["\xa0"]] "/%C2%A0"
+    , Test "{/a}" ["a" =: d ["A" =: "1"]] "/A,1"
+    , Test "{/a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "/A,1,B,2"
+    , Test "{/a}" ["a" =: d ["A" =: "%"]] "/A,%25"
+    , Test "{/a}" ["a" =: d ["A" =: "\xa0"]] "/A,%C2%A0"
+    , Test "{/a}" ["a" =: d ["%" =: "1"]] "/%25,1"
+    , Test "{/a*}" [] ""
+    , Test "{/a*}" ["a" =: s ""] "/"
+    , Test "{/a*}" ["a" =: s "A"] "/A"
+    , Test "{/a*}" ["a" =: l []] ""
+    , Test "{/a*}" ["a" =: l ["A"]] "/A"
+    , Test "{/a*}" ["a" =: l ["A", "B"]] "/A/B"
+    , Test "{/a*}" ["a" =: d []] ""
+    , Test "{/a*}" ["a" =: d ["A" =: "1"]] "/A=1"
+    , Test "{/a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "/A=1/B=2"
+    , Test "{/a:1}" [] ""
+    , Test "{/a:1}" ["a" =: s ""] "/"
+    , Test "{/a:1}" ["a" =: s "A"] "/A"
+    , Test "{/a:1}" ["a" =: s "AB"] "/A"
+    , Test "{/a:1}" ["a" =: s "%B"] "/%25"
+    , Test "{/a:1}" ["a" =: s "\xa0\&B"] "/%C2%A0"
+    , Test "{/a:1}" ["a" =: s "\xd7ff\&B"] "/%ED%9F%BF"
+    , Test "{/a:1}" ["a" =: s "\x10000\&B"] "/%F0%90%80%80"
+    , Test "{/a:1}" ["a" =: l []] ""
+    , Test "{/a:1}" ["a" =: l ["AB"]] "/AB"
+    , Test "{/a:1}" ["a" =: l ["AB", "CD"]] "/AB,CD"
+    , Test "{/a:1}" ["a" =: d []] ""
+    , Test "{/a:1}" ["a" =: d ["AB" =: "12"]] "/AB,12"
+    , Test "{/a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "/AB,12,CD,34"
+    , Test "{/a,a}" [] ""
+    , Test "{/a,a}" ["a" =: l []] ""
+    , Test "{/a,a}" ["a" =: d []] ""
+    , Test "{/a,a}" ["a" =: s ""] "//"
+    , Test "{/a,b}" ["a" =: s ""] "/"
+    , Test "{/a,b}" ["b" =: s ""] "/"
+    , Test "{/%aa}" ["%aa" =: s "A"] "/A"
+    , Test "{/%aa}" ["%aa" =: l ["A", "B"]] "/A,B"
+    , Test "{/%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "/A,1,B,2"
+    , Test "{/%aa*}" ["%aa" =: s "A"] "/A"
+    , Test "{/%aa*}" ["%aa" =: l ["A", "B"]] "/A/B"
+    , Test "{/%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "/A=1/B=2"
+    , Test "{;a}" [] ""
+    , Test "{;a}" ["a" =: l []] ""
+    , Test "{;a}" ["a" =: d []] ""
+    , Test "{;a}" ["a" =: s ""] ";a"
+    , Test "{;a}" ["a" =: s "A"] ";a=A"
+    , Test "{;a}" ["a" =: s "~"] ";a=~"
+    , Test "{;a}" ["a" =: s "%"] ";a=%25"
+    , Test "{;a}" ["a" =: s "?"] ";a=%3F"
+    , Test "{;a}" ["a" =: s "&"] ";a=%26"
+    , Test "{;a}" ["a" =: s "\xa0"] ";a=%C2%A0"
+    , Test "{;a}" ["a" =: s "\xd7ff"] ";a=%ED%9F%BF"
+    , Test "{;a}" ["a" =: s "\x10000"] ";a=%F0%90%80%80"
+    , Test "{;a}" ["a" =: l ["A"]] ";a=A"
+    , Test "{;a}" ["a" =: l ["A", "B"]] ";a=A,B"
+    , Test "{;a}" ["a" =: l ["%"]] ";a=%25"
+    , Test "{;a}" ["a" =: l ["\xa0"]] ";a=%C2%A0"
+    , Test "{;a}" ["a" =: d ["A" =: "1"]] ";a=A,1"
+    , Test "{;a}" ["a" =: d ["A" =: "1", "B" =: "2"]] ";a=A,1,B,2"
+    , Test "{;a}" ["a" =: d ["A" =: "%"]] ";a=A,%25"
+    , Test "{;a}" ["a" =: d ["A" =: "\xa0"]] ";a=A,%C2%A0"
+    , Test "{;a}" ["a" =: d ["%" =: "1"]] ";a=%25,1"
+    , Test "{;a*}" [] ""
+    , Test "{;a*}" ["a" =: s ""] ";a"
+    , Test "{;a*}" ["a" =: s "A"] ";a=A"
+    , Test "{;a*}" ["a" =: l []] ""
+    , Test "{;a*}" ["a" =: l ["A"]] ";a=A"
+    , Test "{;a*}" ["a" =: l ["A", "B"]] ";a=A;a=B"
+    , Test "{;a*}" ["a" =: d []] ""
+    , Test "{;a*}" ["a" =: d ["A" =: "1"]] ";A=1"
+    , Test "{;a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] ";A=1;B=2"
+    , Test "{;a:1}" [] ""
+    , Test "{;a:1}" ["a" =: s ""] ";a"
+    , Test "{;a:1}" ["a" =: s "A"] ";a=A"
+    , Test "{;a:1}" ["a" =: s "AB"] ";a=A"
+    , Test "{;a:1}" ["a" =: s "%B"] ";a=%25"
+    , Test "{;a:1}" ["a" =: s "\xa0\&B"] ";a=%C2%A0"
+    , Test "{;a:1}" ["a" =: s "\xd7ff\&B"] ";a=%ED%9F%BF"
+    , Test "{;a:1}" ["a" =: s "\x10000\&B"] ";a=%F0%90%80%80"
+    , Test "{;a:1}" ["a" =: l []] ""
+    , Test "{;a:1}" ["a" =: l ["AB"]] ";a=AB"
+    , Test "{;a:1}" ["a" =: l ["AB", "CD"]] ";a=AB,CD"
+    , Test "{;a:1}" ["a" =: d []] ""
+    , Test "{;a:1}" ["a" =: d ["AB" =: "12"]] ";a=AB,12"
+    , Test "{;a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] ";a=AB,12,CD,34"
+    , Test "{;a,a}" [] ""
+    , Test "{;a,a}" ["a" =: l []] ""
+    , Test "{;a,a}" ["a" =: d []] ""
+    , Test "{;a,a}" ["a" =: s ""] ";a;a"
+    , Test "{;a,b}" ["a" =: s ""] ";a"
+    , Test "{;a,b}" ["b" =: s ""] ";b"
+    , Test "{;%aa}" ["%aa" =: s "A"] ";%aa=A"
+    , Test "{;%aa}" ["%aa" =: l ["A", "B"]] ";%aa=A,B"
+    , Test "{;%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] ";%aa=A,1,B,2"
+    , Test "{;%aa*}" ["%aa" =: s "A"] ";%aa=A"
+    , Test "{;%aa*}" ["%aa" =: l ["A", "B"]] ";%aa=A;%aa=B"
+    , Test "{;%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] ";A=1;B=2"
+    , Test "{?a}" [] ""
+    , Test "{?a}" ["a" =: l []] ""
+    , Test "{?a}" ["a" =: d []] ""
+    , Test "{?a}" ["a" =: s ""] "?a="
+    , Test "{?a}" ["a" =: s "A"] "?a=A"
+    , Test "{?a}" ["a" =: s "~"] "?a=~"
+    , Test "{?a}" ["a" =: s "%"] "?a=%25"
+    , Test "{?a}" ["a" =: s "?"] "?a=%3F"
+    , Test "{?a}" ["a" =: s "&"] "?a=%26"
+    , Test "{?a}" ["a" =: s "\xa0"] "?a=%C2%A0"
+    , Test "{?a}" ["a" =: s "\xd7ff"] "?a=%ED%9F%BF"
+    , Test "{?a}" ["a" =: s "\x10000"] "?a=%F0%90%80%80"
+    , Test "{?a}" ["a" =: l ["A"]] "?a=A"
+    , Test "{?a}" ["a" =: l ["A", "B"]] "?a=A,B"
+    , Test "{?a}" ["a" =: l ["%"]] "?a=%25"
+    , Test "{?a}" ["a" =: l ["\xa0"]] "?a=%C2%A0"
+    , Test "{?a}" ["a" =: d ["A" =: "1"]] "?a=A,1"
+    , Test "{?a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "?a=A,1,B,2"
+    , Test "{?a}" ["a" =: d ["A" =: "%"]] "?a=A,%25"
+    , Test "{?a}" ["a" =: d ["A" =: "\xa0"]] "?a=A,%C2%A0"
+    , Test "{?a}" ["a" =: d ["%" =: "1"]] "?a=%25,1"
+    , Test "{?a*}" [] ""
+    , Test "{?a*}" ["a" =: s ""] "?a="
+    , Test "{?a*}" ["a" =: s "A"] "?a=A"
+    , Test "{?a*}" ["a" =: l []] ""
+    , Test "{?a*}" ["a" =: l ["A"]] "?a=A"
+    , Test "{?a*}" ["a" =: l ["A", "B"]] "?a=A&a=B"
+    , Test "{?a*}" ["a" =: d []] ""
+    , Test "{?a*}" ["a" =: d ["A" =: "1"]] "?A=1"
+    , Test "{?a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "?A=1&B=2"
+    , Test "{?a:1}" [] ""
+    , Test "{?a:1}" ["a" =: s ""] "?a="
+    , Test "{?a:1}" ["a" =: s "A"] "?a=A"
+    , Test "{?a:1}" ["a" =: s "AB"] "?a=A"
+    , Test "{?a:1}" ["a" =: s "%B"] "?a=%25"
+    , Test "{?a:1}" ["a" =: s "\xa0\&B"] "?a=%C2%A0"
+    , Test "{?a:1}" ["a" =: s "\xd7ff\&B"] "?a=%ED%9F%BF"
+    , Test "{?a:1}" ["a" =: s "\x10000\&B"] "?a=%F0%90%80%80"
+    , Test "{?a:1}" ["a" =: l []] ""
+    , Test "{?a:1}" ["a" =: l ["AB"]] "?a=AB"
+    , Test "{?a:1}" ["a" =: l ["AB", "CD"]] "?a=AB,CD"
+    , Test "{?a:1}" ["a" =: d []] ""
+    , Test "{?a:1}" ["a" =: d ["AB" =: "12"]] "?a=AB,12"
+    , Test "{?a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "?a=AB,12,CD,34"
+    , Test "{?a,a}" [] ""
+    , Test "{?a,a}" ["a" =: l []] ""
+    , Test "{?a,a}" ["a" =: d []] ""
+    , Test "{?a,a}" ["a" =: s ""] "?a=&a="
+    , Test "{?a,b}" ["a" =: s ""] "?a="
+    , Test "{?a,b}" ["b" =: s ""] "?b="
+    , Test "{?%aa}" ["%aa" =: s "A"] "?%aa=A"
+    , Test "{?%aa}" ["%aa" =: l ["A", "B"]] "?%aa=A,B"
+    , Test "{?%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "?%aa=A,1,B,2"
+    , Test "{?%aa*}" ["%aa" =: s "A"] "?%aa=A"
+    , Test "{?%aa*}" ["%aa" =: l ["A", "B"]] "?%aa=A&%aa=B"
+    , Test "{?%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "?A=1&B=2"
+    , Test "{&a}" [] ""
+    , Test "{&a}" ["a" =: l []] ""
+    , Test "{&a}" ["a" =: d []] ""
+    , Test "{&a}" ["a" =: s ""] "&a="
+    , Test "{&a}" ["a" =: s "A"] "&a=A"
+    , Test "{&a}" ["a" =: s "~"] "&a=~"
+    , Test "{&a}" ["a" =: s "%"] "&a=%25"
+    , Test "{&a}" ["a" =: s "?"] "&a=%3F"
+    , Test "{&a}" ["a" =: s "&"] "&a=%26"
+    , Test "{&a}" ["a" =: s "\xa0"] "&a=%C2%A0"
+    , Test "{&a}" ["a" =: s "\xd7ff"] "&a=%ED%9F%BF"
+    , Test "{&a}" ["a" =: s "\x10000"] "&a=%F0%90%80%80"
+    , Test "{&a}" ["a" =: l ["A"]] "&a=A"
+    , Test "{&a}" ["a" =: l ["A", "B"]] "&a=A,B"
+    , Test "{&a}" ["a" =: l ["%"]] "&a=%25"
+    , Test "{&a}" ["a" =: l ["\xa0"]] "&a=%C2%A0"
+    , Test "{&a}" ["a" =: d ["A" =: "1"]] "&a=A,1"
+    , Test "{&a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "&a=A,1,B,2"
+    , Test "{&a}" ["a" =: d ["A" =: "%"]] "&a=A,%25"
+    , Test "{&a}" ["a" =: d ["A" =: "\xa0"]] "&a=A,%C2%A0"
+    , Test "{&a}" ["a" =: d ["%" =: "1"]] "&a=%25,1"
+    , Test "{&a*}" [] ""
+    , Test "{&a*}" ["a" =: s ""] "&a="
+    , Test "{&a*}" ["a" =: s "A"] "&a=A"
+    , Test "{&a*}" ["a" =: l []] ""
+    , Test "{&a*}" ["a" =: l ["A"]] "&a=A"
+    , Test "{&a*}" ["a" =: l ["A", "B"]] "&a=A&a=B"
+    , Test "{&a*}" ["a" =: d []] ""
+    , Test "{&a*}" ["a" =: d ["A" =: "1"]] "&A=1"
+    , Test "{&a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "&A=1&B=2"
+    , Test "{&a:1}" [] ""
+    , Test "{&a:1}" ["a" =: s ""] "&a="
+    , Test "{&a:1}" ["a" =: s "A"] "&a=A"
+    , Test "{&a:1}" ["a" =: s "AB"] "&a=A"
+    , Test "{&a:1}" ["a" =: s "%B"] "&a=%25"
+    , Test "{&a:1}" ["a" =: s "\xa0\&B"] "&a=%C2%A0"
+    , Test "{&a:1}" ["a" =: s "\xd7ff\&B"] "&a=%ED%9F%BF"
+    , Test "{&a:1}" ["a" =: s "\x10000\&B"] "&a=%F0%90%80%80"
+    , Test "{&a:1}" ["a" =: l []] ""
+    , Test "{&a:1}" ["a" =: l ["AB"]] "&a=AB"
+    , Test "{&a:1}" ["a" =: l ["AB", "CD"]] "&a=AB,CD"
+    , Test "{&a:1}" ["a" =: d []] ""
+    , Test "{&a:1}" ["a" =: d ["AB" =: "12"]] "&a=AB,12"
+    , Test "{&a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "&a=AB,12,CD,34"
+    , Test "{&a,a}" [] ""
+    , Test "{&a,a}" ["a" =: l []] ""
+    , Test "{&a,a}" ["a" =: d []] ""
+    , Test "{&a,a}" ["a" =: s ""] "&a=&a="
+    , Test "{&a,b}" ["a" =: s ""] "&a="
+    , Test "{&a,b}" ["b" =: s ""] "&b="
+    , Test "{&%aa}" ["%aa" =: s "A"] "&%aa=A"
+    , Test "{&%aa}" ["%aa" =: l ["A", "B"]] "&%aa=A,B"
+    , Test "{&%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "&%aa=A,1,B,2"
+    , Test "{&%aa*}" ["%aa" =: s "A"] "&%aa=A"
+    , Test "{&%aa*}" ["%aa" =: l ["A", "B"]] "&%aa=A&%aa=B"
+    , Test "{&%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "&A=1&B=2"
+    ]
+  , let values = ["%AA" =: s "1", "%Aa" =: s "2", "%aA" =: s "3", "%aa" =: s "4"]
+    in
+      [ Test "{%AA}" values "1"
+      , Test "{%Aa}" values "2"
+      , Test "{%aA}" values "3"
+      , Test "{%aa}" values "4"
+          -- This comment forces Brittany to use a multi-line layout.
+      ]
+  , let values = ["a" =: s "abcdefghijklmnopqrstuvwxyz"]
+    in
+      [ Test "{a:1}" values "a"
+      , Test "{a:5}" values "abcde"
+      , Test "{a:10}" values "abcdefghij"
+      , Test "{a:15}" values "abcdefghijklmno"
+      , Test "{a:20}" values "abcdefghijklmnopqrst"
+      , Test "{a:25}" values "abcdefghijklmnopqrstuvwxy"
+      , Test "{a:26}" values "abcdefghijklmnopqrstuvwxyz"
+      , Test "{a:30}" values "abcdefghijklmnopqrstuvwxyz"
+      ]
+  , let values = ["a" =: l []]
+    in
+      [ Test "{a}" values ""
+      , Test "{+a}" values ""
+      , Test "{#a}" values ""
+      , Test "{.a}" values ""
+      , Test "{/a}" values ""
+      , Test "{;a}" values ""
+      , Test "{?a}" values ""
+      , Test "{&a}" values ""
+      ]
+  , let values = ["a" =: d []]
+    in
+      [ Test "{a}" values ""
+      , Test "{+a}" values ""
+      , Test "{#a}" values ""
+      , Test "{.a}" values ""
+      , Test "{/a}" values ""
+      , Test "{;a}" values ""
+      , Test "{?a}" values ""
+      , Test "{&a}" values ""
+      ]
+  , let values = ["a" =: s ""]
+    in
+      [ Test "{a}" values ""
+      , Test "{+a}" values ""
+      , Test "{#a}" values "#"
+      , Test "{.a}" values "."
+      , Test "{/a}" values "/"
+      , Test "{;a}" values ";a"
+      , Test "{?a}" values "?a="
+      , Test "{&a}" values "&a="
+      ]
+  , let values = ["a" =: s "A"]
+    in
+      [ Test "{a}" values "A"
+      , Test "{+a}" values "A"
+      , Test "{#a}" values "#A"
+      , Test "{.a}" values ".A"
+      , Test "{/a}" values "/A"
+      , Test "{;a}" values ";a=A"
+      , Test "{?a}" values "?a=A"
+      , Test "{&a}" values "&a=A"
+      ]
+  , let values = ["b" =: s "B"]
+    in
+      [ Test "{a,b}" values "B"
+      , Test "{+a,b}" values "B"
+      , Test "{#a,b}" values "#B"
+      , Test "{.a,b}" values ".B"
+      , Test "{/a,b}" values "/B"
+      , Test "{;a,b}" values ";b=B"
+      , Test "{?a,b}" values "?b=B"
+      , Test "{&a,b}" values "&b=B"
+      ]
+  , let values = ["a" =: s ""]
+    in
+      [ Test "{a,a}" values ","
+      , Test "{+a,a}" values ","
+      , Test "{#a,a}" values "#,"
+      , Test "{.a,a}" values ".."
+      , Test "{/a,a}" values "//"
+      , Test "{;a,a}" values ";a;a"
+      , Test "{?a,a}" values "?a=&a="
+      , Test "{&a,a}" values "&a=&a="
+      ]
+  , let values = ["a" =: s "A", "b" =: s "B"]
+    in
+      [ Test "{a,b}" values "A,B"
+      , Test "{+a,b}" values "A,B"
+      , Test "{#a,b}" values "#A,B"
+      , Test "{.a,b}" values ".A.B"
+      , Test "{/a,b}" values "/A/B"
+      , Test "{;a,b}" values ";a=A;b=B"
+      , Test "{?a,b}" values "?a=A&b=B"
+      , Test "{&a,b}" values "&a=A&b=B"
+      ]
+  , let values = ["a" =: d ["K! \xa0\xd7ff\x10000" =: "V! \xa0\xd7ff\x10000"]]
+    in
+      [ Test "{a*}" values "K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
+      , Test "{+a*}" values "K!%20%C2%A0%ED%9F%BF%F0%90%80%80=V!%20%C2%A0%ED%9F%BF%F0%90%80%80"
+      , Test "{#a*}" values "#K!%20%C2%A0%ED%9F%BF%F0%90%80%80=V!%20%C2%A0%ED%9F%BF%F0%90%80%80"
+      , Test "{.a*}" values ".K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
+      , Test "{/a*}" values "/K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
+      , Test "{;a*}" values ";K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
+      , Test "{?a*}" values "?K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
+      , Test "{&a*}" values "&K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80"
+      ]
+  , let
+      values =
+        [ "empty" =: s ""
+        , "hello" =: s "Hello World!"
+        , "keys" =: d ["semi" =: ";", "dot" =: ".", "comma" =: ","]
+        , "list" =: l ["red", "green", "blue"]
+        , "path" =: s "/foo/bar"
+        , "var" =: s "value"
+        , "x" =: s "1024"
+        , "y" =: s "768"
+        ]
+    in
+      [ Test "{var}" values "value"
+      , Test "{hello}" values "Hello%20World%21"
+      , Test "{+var}" values "value"
+      , Test "{+hello}" values "Hello%20World!"
+      , Test "{+path}/here" values "/foo/bar/here"
+      , Test "here?ref={+path}" values "here?ref=/foo/bar"
+      , Test "X{#var}" values "X#value"
+      , Test "X{#hello}" values "X#Hello%20World!"
+      , Test "map?{x,y}" values "map?1024,768"
+      , Test "{x,hello,y}" values "1024,Hello%20World%21,768"
+      , Test "{+x,hello,y}" values "1024,Hello%20World!,768"
+      , Test "{+path,x}/here" values "/foo/bar,1024/here"
+      , Test "{#x,hello,y}" values "#1024,Hello%20World!,768"
+      , Test "{#path,x}/here" values "#/foo/bar,1024/here"
+      , Test "X{.var}" values "X.value"
+      , Test "X{.x,y}" values "X.1024.768"
+      , Test "{/var}" values "/value"
+      , Test "{/var,x}/here" values "/value/1024/here"
+      , Test "{;x,y}" values ";x=1024;y=768"
+      , Test "{;x,y,empty}" values ";x=1024;y=768;empty"
+      , Test "{?x,y}" values "?x=1024&y=768"
+      , Test "{?x,y,empty}" values "?x=1024&y=768&empty="
+      , Test "?fixed=yes{&x}" values "?fixed=yes&x=1024"
+      , Test "{&x,y,empty}" values "&x=1024&y=768&empty="
+      , Test "{var:3}" values "val"
+      , Test "{var:30}" values "value"
+      , Test "{list}" values "red,green,blue"
+      , Test "{list*}" values "red,green,blue"
+      , Test "{keys}" values "comma,%2C,dot,.,semi,%3B"
+      , Test "{keys*}" values "comma=%2C,dot=.,semi=%3B"
+      , Test "{+path:6}/here" values "/foo/b/here"
+      , Test "{+list}" values "red,green,blue"
+      , Test "{+list*}" values "red,green,blue"
+      , Test "{+keys}" values "comma,,,dot,.,semi,;"
+      , Test "{+keys*}" values "comma=,,dot=.,semi=;"
+      , Test "{#path:6}/here" values "#/foo/b/here"
+      , Test "{#list}" values "#red,green,blue"
+      , Test "{#list*}" values "#red,green,blue"
+      , Test "{#keys}" values "#comma,,,dot,.,semi,;"
+      , Test "{#keys*}" values "#comma=,,dot=.,semi=;"
+      , Test "X{.var:3}" values "X.val"
+      , Test "X{.list}" values "X.red,green,blue"
+      , Test "X{.list*}" values "X.red.green.blue"
+      , Test "X{.keys}" values "X.comma,%2C,dot,.,semi,%3B"
+      , Test "X{.keys*}" values "X.comma=%2C.dot=..semi=%3B"
+      , Test "{/var:1,var}" values "/v/value"
+      , Test "{/list}" values "/red,green,blue"
+      , Test "{/list*}" values "/red/green/blue"
+      , Test "{/list*,path:4}" values "/red/green/blue/%2Ffoo"
+      , Test "{/keys}" values "/comma,%2C,dot,.,semi,%3B"
+      , Test "{/keys*}" values "/comma=%2C/dot=./semi=%3B"
+      , Test "{;hello:5}" values ";hello=Hello"
+      , Test "{;list}" values ";list=red,green,blue"
+      , Test "{;list*}" values ";list=red;list=green;list=blue"
+      , Test "{;keys}" values ";keys=comma,%2C,dot,.,semi,%3B"
+      , Test "{;keys*}" values ";comma=%2C;dot=.;semi=%3B"
+      , Test "{?var:3}" values "?var=val"
+      , Test "{?list}" values "?list=red,green,blue"
+      , Test "{?list*}" values "?list=red&list=green&list=blue"
+      , Test "{?keys}" values "?keys=comma,%2C,dot,.,semi,%3B"
+      , Test "{?keys*}" values "?comma=%2C&dot=.&semi=%3B"
+      , Test "{&var:3}" values "&var=val"
+      , Test "{&list}" values "&list=red,green,blue"
+      , Test "{&list*}" values "&list=red&list=green&list=blue"
+      , Test "{&keys}" values "&keys=comma,%2C,dot,.,semi,%3B"
+      , Test "{&keys*}" values "&comma=%2C&dot=.&semi=%3B"
+      ]
+  , let values = ["var" =: s "value", "semi" =: s ";"]
+    in
+      [ Test "{var}" values "value"
+      , Test "{var:20}" values "value"
+      , Test "{var:3}" values "val"
+      , Test "{semi}" values "%3B"
+      , Test "{semi:2}" values "%3B"
+          -- This comment forces Brittany to use a multi-line layout.
+      ]
+  , let values = ["year" =: l ["1965", "2000", "2012"], "dom" =: l ["example", "com"]]
+    in
+      [ Test "find{?year*}" values "find?year=1965&year=2000&year=2012"
+      , Test "www{.dom*}" values "www.example.com"
+          -- This comment forces Brittany to use a multi-line layout.
+      ]
+  , let
+      values =
+        [ "base" =: s "http://example.com/home/"
+        , "count" =: l ["one", "two", "three"]
+        , "dom" =: l ["example", "com"]
+        , "dub" =: s "me/too"
+        , "empty_keys" =: d []
+        , "empty" =: s ""
+        , "half" =: s "50%"
+        , "hello" =: s "Hello World!"
+        , "keys" =: d ["semi" =: ";", "dot" =: ".", "comma" =: ","]
+        , "list" =: l ["red", "green", "blue"]
+        , "path" =: s "/foo/bar"
+        , "v" =: s "6"
+        , "var" =: s "value"
+        , "who" =: s "fred"
+        , "x" =: s "1024"
+        , "y" =: s "768"
+        ]
+    in
+      [ Test "{count}" values "one,two,three"
+      , Test "{count*}" values "one,two,three"
+      , Test "{/count}" values "/one,two,three"
+      , Test "{/count*}" values "/one/two/three"
+      , Test "{;count}" values ";count=one,two,three"
+      , Test "{;count*}" values ";count=one;count=two;count=three"
+      , Test "{?count}" values "?count=one,two,three"
+      , Test "{?count*}" values "?count=one&count=two&count=three"
+      , Test "{&count*}" values "&count=one&count=two&count=three"
+      , Test "{var}" values "value"
+      , Test "{hello}" values "Hello%20World%21"
+      , Test "{half}" values "50%25"
+      , Test "O{empty}X" values "OX"
+      , Test "O{undef}X" values "OX"
+      , Test "{x,y}" values "1024,768"
+      , Test "{x,hello,y}" values "1024,Hello%20World%21,768"
+      , Test "?{x,empty}" values "?1024,"
+      , Test "?{x,undef}" values "?1024"
+      , Test "?{undef,y}" values "?768"
+      , Test "{var:3}" values "val"
+      , Test "{var:30}" values "value"
+      , Test "{list}" values "red,green,blue"
+      , Test "{list*}" values "red,green,blue"
+      , Test "{keys}" values "comma,%2C,dot,.,semi,%3B"
+      , Test "{keys*}" values "comma=%2C,dot=.,semi=%3B"
+      , Test "{+var}" values "value"
+      , Test "{+hello}" values "Hello%20World!"
+      , Test "{+half}" values "50%25"
+      , Test "{base}index" values "http%3A%2F%2Fexample.com%2Fhome%2Findex"
+      , Test "{+base}index" values "http://example.com/home/index"
+      , Test "O{+empty}X" values "OX"
+      , Test "O{+undef}X" values "OX"
+      , Test "{+path}/here" values "/foo/bar/here"
+      , Test "here?ref={+path}" values "here?ref=/foo/bar"
+      , Test "up{+path}{var}/here" values "up/foo/barvalue/here"
+      , Test "{+x,hello,y}" values "1024,Hello%20World!,768"
+      , Test "{+path,x}/here" values "/foo/bar,1024/here"
+      , Test "{+path:6}/here" values "/foo/b/here"
+      , Test "{+list}" values "red,green,blue"
+      , Test "{+list*}" values "red,green,blue"
+      , Test "{+keys}" values "comma,,,dot,.,semi,;"
+      , Test "{+keys*}" values "comma=,,dot=.,semi=;"
+      , Test "{#var}" values "#value"
+      , Test "{#hello}" values "#Hello%20World!"
+      , Test "{#half}" values "#50%25"
+      , Test "foo{#empty}" values "foo#"
+      , Test "foo{#undef}" values "foo"
+      , Test "{#x,hello,y}" values "#1024,Hello%20World!,768"
+      , Test "{#path,x}/here" values "#/foo/bar,1024/here"
+      , Test "{#path:6}/here" values "#/foo/b/here"
+      , Test "{#list}" values "#red,green,blue"
+      , Test "{#list*}" values "#red,green,blue"
+      , Test "{#keys}" values "#comma,,,dot,.,semi,;"
+      , Test "{#keys*}" values "#comma=,,dot=.,semi=;"
+      , Test "{.who}" values ".fred"
+      , Test "{.who,who}" values ".fred.fred"
+      , Test "{.half,who}" values ".50%25.fred"
+      , Test "www{.dom*}" values "www.example.com"
+      , Test "X{.var}" values "X.value"
+      , Test "X{.empty}" values "X."
+      , Test "X{.undef}" values "X"
+      , Test "X{.var:3}" values "X.val"
+      , Test "X{.list}" values "X.red,green,blue"
+      , Test "X{.list*}" values "X.red.green.blue"
+      , Test "X{.keys}" values "X.comma,%2C,dot,.,semi,%3B"
+      , Test "X{.keys*}" values "X.comma=%2C.dot=..semi=%3B"
+      , Test "X{.empty_keys}" values "X"
+      , Test "X{.empty_keys*}" values "X"
+      , Test "{/who}" values "/fred"
+      , Test "{/who,who}" values "/fred/fred"
+      , Test "{/half,who}" values "/50%25/fred"
+      , Test "{/who,dub}" values "/fred/me%2Ftoo"
+      , Test "{/var}" values "/value"
+      , Test "{/var,empty}" values "/value/"
+      , Test "{/var,undef}" values "/value"
+      , Test "{/var,x}/here" values "/value/1024/here"
+      , Test "{/var:1,var}" values "/v/value"
+      , Test "{/list}" values "/red,green,blue"
+      , Test "{/list*}" values "/red/green/blue"
+      , Test "{/list*,path:4}" values "/red/green/blue/%2Ffoo"
+      , Test "{/keys}" values "/comma,%2C,dot,.,semi,%3B"
+      , Test "{/keys*}" values "/comma=%2C/dot=./semi=%3B"
+      , Test "{;who}" values ";who=fred"
+      , Test "{;half}" values ";half=50%25"
+      , Test "{;empty}" values ";empty"
+      , Test "{;v,empty,who}" values ";v=6;empty;who=fred"
+      , Test "{;v,bar,who}" values ";v=6;who=fred"
+      , Test "{;x,y}" values ";x=1024;y=768"
+      , Test "{;x,y,empty}" values ";x=1024;y=768;empty"
+      , Test "{;x,y,undef}" values ";x=1024;y=768"
+      , Test "{;hello:5}" values ";hello=Hello"
+      , Test "{;list}" values ";list=red,green,blue"
+      , Test "{;list*}" values ";list=red;list=green;list=blue"
+      , Test "{;keys}" values ";keys=comma,%2C,dot,.,semi,%3B"
+      , Test "{;keys*}" values ";comma=%2C;dot=.;semi=%3B"
+      , Test "{?who}" values "?who=fred"
+      , Test "{?half}" values "?half=50%25"
+      , Test "{?x,y}" values "?x=1024&y=768"
+      , Test "{?x,y,empty}" values "?x=1024&y=768&empty="
+      , Test "{?x,y,undef}" values "?x=1024&y=768"
+      , Test "{?var:3}" values "?var=val"
+      , Test "{?list}" values "?list=red,green,blue"
+      , Test "{?list*}" values "?list=red&list=green&list=blue"
+      , Test "{?keys}" values "?keys=comma,%2C,dot,.,semi,%3B"
+      , Test "{?keys*}" values "?comma=%2C&dot=.&semi=%3B"
+      , Test "{&who}" values "&who=fred"
+      , Test "{&half}" values "&half=50%25"
+      , Test "?fixed=yes{&x}" values "?fixed=yes&x=1024"
+      , Test "{&x,y,empty}" values "&x=1024&y=768&empty="
+      , Test "{&x,y,undef}" values "&x=1024&y=768"
+      , Test "{&var:3}" values "&var=val"
+      , Test "{&list}" values "&list=red,green,blue"
+      , Test "{&list*}" values "&list=red&list=green&list=blue"
+      , Test "{&keys}" values "&keys=comma,%2C,dot,.,semi,%3B"
+      , Test "{&keys*}" values "&comma=%2C&dot=.&semi=%3B"
+      ]
+  ]
+
+data Test = Test
+  { testInput :: String
+  , testValues :: [(String, Burrito.Value)]
+  , testOutput :: Output
+  } deriving (Eq, Show)
+
+runTest :: Test -> Hspec.Expectation
+runTest test =
+  case (Burrito.parse $ testInput test, unwrapOutput $ testOutput test) of
+    (Nothing, Nothing) -> pure ()
+    (Nothing, Just _) -> Hspec.expectationFailure "should have parsed"
+    (Just _, Nothing) -> Hspec.expectationFailure "should not have parsed"
+    (Just template, Just expected) -> do
+      let
+        values = testValues test
+        actual = Burrito.expand values template
+      actual `Hspec.shouldBe` expected
+      Burrito.parse (Burrito.render template) `Hspec.shouldBe` Just template
+      let
+        relevant =
+          List.sort $ keepRelevant (templateVariables template) values
+      Monad.when (isMatchable template relevant) $ do
+        let matches = List.sort <$> Burrito.match expected template
+        matches `Hspec.shouldSatisfy` elem relevant
+
+isMatchable :: Template.Template -> [(String, Burrito.Value)] -> Bool
+isMatchable template values =
+  (not . any (isAsterisk . Variable.modifier) $ templateVariables template)
+    && all (isString . snd) values
+
+isString :: Burrito.Value -> Bool
+isString value = case value of
+  Value.String _ -> True
+  _ -> False
+
+isAsterisk :: Modifier.Modifier -> Bool
+isAsterisk modifier = case modifier of
+  Modifier.Asterisk -> True
+  _ -> False
+
+keepRelevant
+  :: Set.Set Variable.Variable
+  -> [(String, Burrito.Value)]
+  -> [(String, Burrito.Value)]
+keepRelevant variables =
+  let
+    vs =
+      Map.fromListWith
+          (\mx my -> case (mx, my) of
+            (Just x, Just y) -> Just $ max x y
+            _ -> Nothing
+          )
+        . fmap
+            (\v ->
+              ( Render.builderToString . Render.name $ Variable.name v
+              , case Variable.modifier v of
+                Modifier.Colon n -> Just $ MaxLength.count n
+                _ -> Nothing
+              )
+            )
+        $ Set.toList variables
+  in
+    Maybe.mapMaybe $ \(k, v) -> do
+      m <- Map.lookup k vs
+      pure . (,) k $ case m of
+        Nothing -> v
+        Just n -> case v of
+          Value.String t -> Value.String $ Text.take n t
+          _ -> v
+
+templateVariables :: Template.Template -> Set.Set Variable.Variable
+templateVariables =
+  Set.fromList
+    . concatMap
+        (\token -> case token of
+          Token.Expression expression ->
+            NonEmpty.toList $ Expression.variables expression
+          Token.Literal _ -> []
+        )
+    . Template.tokens
+
+newtype Output = Output
+  { unwrapOutput :: Maybe String
+  } deriving (Eq, Show)
+
+instance String.IsString Output where
+  fromString = Output . Just
+
+noParse :: Output
+noParse = Output Nothing
+
+(=:) :: a -> b -> (a, b)
+(=:) = (,)
+
+s :: String -> Burrito.Value
+s = Burrito.stringValue
+
+l :: [String] -> Burrito.Value
+l = Burrito.listValue
+
+d :: [(String, String)] -> Burrito.Value
+d = Burrito.dictionaryValue
+
+newtype Template = Template
+  { unwrapTemplate :: Template.Template
+  } deriving (Eq)
+
+instance Show Template where
+  show (Template template) =
+    unwords [show $ Burrito.render template, "{-", show template, "-}"]
+
+instance QC.Arbitrary Template where
+  arbitrary = Template <$> arbitraryTemplate
+  shrink = fmap Template . shrinkTemplate . unwrapTemplate
+
+type Shrink a = a -> [a]
+
+arbitraryTemplate :: QC.Gen Template.Template
+arbitraryTemplate = Template.Template . simplify <$> QC.listOf arbitraryToken
+
+shrinkTemplate :: Shrink Template.Template
+shrinkTemplate =
+  fmap (Template.Template . simplify)
+    . QC.shrinkList shrinkToken
+    . Template.tokens
+
+simplify :: [Token.Token] -> [Token.Token]
+simplify tokens = case tokens of
+  t1 : t2 : ts -> case (t1, t2) of
+    (Token.Literal l1, Token.Literal l2) ->
+      simplify $ Token.Literal (appendLiteral l1 l2) : ts
+    _ -> t1 : simplify (t2 : ts)
+  _ -> tokens
+
+appendLiteral :: Literal.Literal -> Literal.Literal -> Literal.Literal
+appendLiteral x y =
+  Literal.Literal $ Literal.characters x <> Literal.characters y
+
+arbitraryToken :: QC.Gen Token.Token
+arbitraryToken = QC.oneof
+  [ Token.Expression <$> arbitraryExpression
+  , Token.Literal <$> arbitraryLiteral
+  ]
+
+shrinkToken :: Shrink Token.Token
+shrinkToken x = case x of
+  Token.Expression y -> Token.Expression <$> shrinkExpression y
+  Token.Literal y -> Token.Literal <$> shrinkLiteral y
+
+arbitraryExpression :: QC.Gen Expression.Expression
+arbitraryExpression =
+  Expression.Expression
+    <$> arbitraryOperator
+    <*> arbitraryNonEmpty arbitraryVariable
+
+shrinkExpression :: Shrink Expression.Expression
+shrinkExpression x = uncurry Expression.Expression <$> shrinkTuple
+  shrinkOperator
+  (shrinkNonEmpty shrinkVariable)
+  (Expression.operator x, Expression.variables x)
+
+arbitraryOperator :: QC.Gen Operator.Operator
+arbitraryOperator = QC.elements
+  [ Operator.Ampersand
+  , Operator.FullStop
+  , Operator.None
+  , Operator.NumberSign
+  , Operator.PlusSign
+  , Operator.QuestionMark
+  , Operator.Semicolon
+  , Operator.Solidus
+  ]
+
+shrinkOperator :: Shrink Operator.Operator
+shrinkOperator x = case x of
+  Operator.None -> []
+  _ -> [Operator.None]
+
+arbitraryNonEmpty :: QC.Gen a -> QC.Gen (NonEmpty.NonEmpty a)
+arbitraryNonEmpty g = (NonEmpty.:|) <$> g <*> QC.listOf g
+
+shrinkNonEmpty :: Shrink a -> Shrink (NonEmpty.NonEmpty a)
+shrinkNonEmpty f x = uncurry (NonEmpty.:|)
+  <$> shrinkTuple f (QC.shrinkList f) (NonEmpty.head x, NonEmpty.tail x)
+
+arbitraryVariable :: QC.Gen Variable.Variable
+arbitraryVariable = Variable.Variable <$> arbitraryName <*> arbitraryModifier
+
+shrinkVariable :: Shrink Variable.Variable
+shrinkVariable variable = uncurry Variable.Variable <$> shrinkTuple
+  shrinkName
+  shrinkModifier
+  (Variable.name variable, Variable.modifier variable)
+
+shrinkTuple :: Shrink a -> Shrink b -> Shrink (a, b)
+shrinkTuple f g (x, y) =
+  fmap (\a -> (a, y)) (f x) <> fmap (\b -> (x, b)) (g y)
+
+arbitraryName :: QC.Gen Name.Name
+arbitraryName = Name.Name <$> arbitraryNonEmpty arbitraryField
+
+shrinkName :: Shrink Name.Name
+shrinkName = fmap Name.Name . shrinkNonEmpty shrinkField . Name.fields
+
+arbitraryField :: QC.Gen Field.Field
+arbitraryField = Field.Field <$> arbitraryNonEmpty arbitraryFieldCharacter
+
+shrinkField :: Shrink Field.Field
+shrinkField =
+  fmap Field.Field . shrinkNonEmpty shrinkFieldCharacter . Field.characters
+
+arbitraryFieldCharacter :: QC.Gen (Character.Character Field.Field)
+arbitraryFieldCharacter = QC.oneof
+  [ Character.Encoded <$> arbitraryDigit <*> arbitraryDigit
+  , Character.Unencoded <$> QC.suchThat QC.arbitrary Parse.isFieldCharacter
+  ]
+
+shrinkFieldCharacter :: Shrink (Character.Character Field.Field)
+shrinkFieldCharacter x = case x of
+  Character.Encoded y z ->
+    uncurry Character.Encoded <$> shrinkTuple shrinkDigit shrinkDigit (y, z)
+  Character.Unencoded y ->
+    fmap Character.Unencoded . filter Parse.isFieldCharacter $ QC.shrink y
+
+arbitraryDigit :: QC.Gen Digit.Digit
+arbitraryDigit = QC.oneof
+  [ pure Digit.Ox0
+  , pure Digit.Ox1
+  , pure Digit.Ox2
+  , pure Digit.Ox3
+  , pure Digit.Ox4
+  , pure Digit.Ox5
+  , pure Digit.Ox6
+  , pure Digit.Ox7
+  , pure Digit.Ox8
+  , pure Digit.Ox9
+  , Digit.OxA <$> arbitraryCase
+  , Digit.OxB <$> arbitraryCase
+  , Digit.OxC <$> arbitraryCase
+  , Digit.OxD <$> arbitraryCase
+  , Digit.OxE <$> arbitraryCase
+  , Digit.OxF <$> arbitraryCase
+  ]
+
+shrinkDigit :: Shrink Digit.Digit
+shrinkDigit x = case x of
+  Digit.Ox0 -> []
+  Digit.Ox1 -> [Digit.Ox0]
+  Digit.Ox2 -> [Digit.Ox0]
+  Digit.Ox3 -> [Digit.Ox0]
+  Digit.Ox4 -> [Digit.Ox0]
+  Digit.Ox5 -> [Digit.Ox0]
+  Digit.Ox6 -> [Digit.Ox0]
+  Digit.Ox7 -> [Digit.Ox0]
+  Digit.Ox8 -> [Digit.Ox0]
+  Digit.Ox9 -> [Digit.Ox0]
+  Digit.OxA y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y)
+  Digit.OxB y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y)
+  Digit.OxC y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y)
+  Digit.OxD y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y)
+  Digit.OxE y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y)
+  Digit.OxF y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y)
+
+arbitraryCase :: QC.Gen Case.Case
+arbitraryCase = QC.elements [Case.Lower, Case.Upper]
+
+shrinkCase :: Shrink Case.Case
+shrinkCase x = case x of
+  Case.Lower -> []
+  Case.Upper -> [Case.Lower]
+
+arbitraryModifier :: QC.Gen Modifier.Modifier
+arbitraryModifier = QC.oneof
+  [ pure Modifier.Asterisk
+  , Modifier.Colon <$> arbitraryMaxLength
+  , pure Modifier.None
+  ]
+
+shrinkModifier :: Shrink Modifier.Modifier
+shrinkModifier x = case x of
+  Modifier.Asterisk -> [Modifier.None]
+  Modifier.Colon y -> Modifier.None : fmap Modifier.Colon (shrinkMaxLength y)
+  Modifier.None -> []
+
+arbitraryMaxLength :: QC.Gen MaxLength.MaxLength
+arbitraryMaxLength =
+  MaxLength.MaxLength <$> QC.suchThat QC.arbitrary Parse.isMaxLength
+
+shrinkMaxLength :: Shrink MaxLength.MaxLength
+shrinkMaxLength =
+  fmap MaxLength.MaxLength
+    . filter Parse.isMaxLength
+    . QC.shrink
+    . MaxLength.count
+
+arbitraryLiteral :: QC.Gen Literal.Literal
+arbitraryLiteral =
+  Literal.Literal <$> arbitraryNonEmpty arbitraryLiteralCharacter
+
+shrinkLiteral :: Shrink Literal.Literal
+shrinkLiteral =
+  fmap Literal.Literal
+    . shrinkNonEmpty shrinkLiteralCharacter
+    . Literal.characters
+
+arbitraryLiteralCharacter :: QC.Gen (Character.Character Literal.Literal)
+arbitraryLiteralCharacter = QC.oneof
+  [ Character.Encoded <$> arbitraryDigit <*> arbitraryDigit
+  , Character.Unencoded <$> QC.suchThat QC.arbitrary Parse.isLiteralCharacter
+  ]
+
+shrinkLiteralCharacter :: Shrink (Character.Character Literal.Literal)
+shrinkLiteralCharacter x = case x of
+  Character.Encoded y z ->
+    uncurry Character.Encoded <$> shrinkTuple shrinkDigit shrinkDigit (y, z)
+  Character.Unencoded y ->
+    fmap Character.Unencoded . filter Parse.isLiteralCharacter $ QC.shrink y
