packages feed

burrito-1.0.1.0: src/test/Main.hs

{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeFamilies #-}

module Main
  ( main
  )
where

import qualified Burrito
import qualified Control.Monad as Monad
import qualified Control.Monad.Trans.Writer as Writer
import qualified Data.Either as Either
import qualified Data.String as String
import qualified GHC.Exts as Exts
import qualified GHC.Stack as Stack
import qualified System.Exit as Exit
import qualified Test.HUnit as Test

main :: IO ()
main = runTests $ do

  label "accepts empty templates" $ do
    test "" [] ""

  label "ignores extra variables" $ do
    test "" ["extra" =: "ignored"] ""

  label "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 "~" [] "~"

  label "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"

  label "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"

  label "passes percent encoded literals through" $ do
    test "%00" [] "%00"

  label "normalizes percent encodings to uppercase" $ do
    test "%AA" [] "%AA"
    test "%Aa" [] "%AA"
    test "%aA" [] "%AA"
    test "%aa" [] "%AA"

  label "rejects invalid percent encodings" $ do
    test "%" [] Failure
    test "%0" [] Failure
    test "%0z" [] Failure
    test "%z" [] Failure

  label "does not decode percent encoded literals" $ do
    test "%30" [] "%30"

  label "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

  label "rejects empty variable names" $ do
    test "{}" [] Failure
    test "{,}" [] Failure
    test "{a,,b}" [] Failure
    test "{+}" [] Failure
    test "{:1}" [] Failure
    test "{*}" [] Failure

  label "accepts uppercase variable names" $ do
    test "{AZ}" [] ""

  label "accepts lowercase variable names" $ do
    test "{az}" [] ""

  label "accepts decimal variable names" $ do
    test "{09}" [] ""

  label "accepts underscores in variable names" $ do
    test "{_a}" [] ""
    test "{a_}" [] ""
    test "{_}" [] ""

  label "accepts dots in variable names" $ do
    test "{A.A}" [] ""
    test "{a.a}" [] ""
    test "{0.0}" [] ""
    test "{_._}" [] ""
    test "{%aa.%aa}" [] ""

  label "rejects invalid dots in variable names" $ do
    test "{.}" [] Failure
    test "{a.}" [] Failure
    test "{+.a}" [] Failure
    test "{a..b}" [] Failure

  label "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.
  label "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"

  label "rejects invalid percent encoded variable names" $ do
    test "{%}" [] Failure
    test "{%0}" [] Failure
    test "{%0z}" [] Failure
    test "{%z}" [] Failure

  label "rejects invalid variable names" $ do
    test "{!}" [] Failure

  label "rejects invalid expressions" $ do
    test "{" [] Failure
    test "{{}" [] Failure
    test "}" [] Failure
    test "{}}" [] Failure

  label "accepts multiple variables in one expression" $ do
    test "{a,b}" [] ""
    test "{a,b,c,d}" [] ""
    test "{a,a}" [] ""

  label "accepts prefix modifiers" $ do
    test "{a:5}" [] ""
    test "{a:67}" [] ""
    test "{a:801}" [] ""
    test "{a:234}" [] ""
    test "{a:9999}" [] ""

  label "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"

  label "rejects invalid prefix modifiers" $ do
    test "{a:}" [] Failure
    test "{a:0}" [] Failure
    test "{a:10000}" [] Failure
    test "{a:-1}" [] Failure

  label "accepts explode modifiers" $ do
    test "{a*}" [] ""

  label "rejects both prefix and explode modifiers" $ do
    test "{a:1*}" [] Failure
    test "{a*:1}" [] Failure

  label "accepts different modifiers on different variables" $ do
    test "{a,b:1,c*}" [] ""

  label "accepts allowed operators" $ do
    test "{+a}" [] ""
    test "{#a}" [] ""
    test "{.a}" [] ""
    test "{/a}" [] ""
    test "{;a}" [] ""
    test "{?a}" [] ""
    test "{&a}" [] ""

  label "rejects reserved operators" $ do
    test "{=a}" [] Failure
    test "{,a}" [] Failure
    test "{!a}" [] Failure
    test "{@a}" [] Failure
    test "{|a}" [] Failure

  label "rejects multiple operators" $ do
    test "{+#a}" [] Failure

  label "rejects different operators for different variables" $ do
    test "{+a,#b}" [] Failure

  label "accepts operators and modifiers" $ do
    test "{+a:1}" [] ""
    test "{#a*}" [] ""

  label "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}" [] ""

  label "accepts multiple expressions" $ do
    test "{a}{b}" [] ""
    test "{a}{b}{c}{d}" [] ""
    test "{a}{a}" [] ""

  label "rejects nested expressions" $ do
    test "{{}}" [] Failure
    test "{a{b}}" [] Failure
    test "{{a}b}" [] Failure
    test "{a{b}c}" [] Failure

  label "accepts literals and expressions together" $ do
    test "a{b}" [] "a"
    test "{a}b" [] "b"
    test "a{b}c" [] "ac"
    test "{a}b{c}" [] "b"

  label "handles missing values" $ do
    test "{a}" [] ""
    test "{+a}" [] ""
    test "{#a}" [] ""
    test "{.a}" [] ""
    test "{/a}" [] ""
    test "{;a}" [] ""
    test "{?a}" [] ""
    test "{&a}" [] ""

  label "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 ""

  label "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 ""

  label "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="

  label "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"

  label "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"

  label "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="

  label "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"

  label "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"

  label "prefers the first variable" $ do
    test "{a}" ["a" =: "A", "a" =: "B"] "A"
    test "{a}" ["a" =: "B", "a" =: "A"] "B"

  label "passes test from rfc" $ do

    label "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"

    label "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

      label "level 1" $ do
        test "{var}" values "value"
        test "{hello}" values "Hello%20World%21"

      label "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!"

      label "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="

      label "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"

    label "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"

    label "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"

    label "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

      label "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"

      label "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"

      label "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=,"

      label "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=,"

      label "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"

      label "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"

      label "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"

      label "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"

      label "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"

  label "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"

  label "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"

  label "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"

  label "handles label 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"

  label "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"

  label "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"

  label "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"

  label "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"

  label "supports quasi quotes" $ do
    label "as expressions" $ do
      let qq x y = Writer.tell . Test $ Just x Test.~?= 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}"

runTests :: Writer.WriterT Test IO a -> IO ()
runTests writer = do
  tests <- Writer.execWriterT writer
  counts <- Test.runTestTT $ unwrapTest tests
  let
    hasErrors = Test.errors counts /= 0
    hasFailures = Test.failures counts /= 0
  Monad.when (hasErrors || hasFailures) Exit.exitFailure

label
  :: Monad m
  => String
  -> Writer.WriterT Test m a
  -> Writer.WriterT Test m a
label string = Writer.censor $ \tests -> Test $ string Test.~: tests

test
  :: (Stack.HasCallStack, Monad m)
  => String -- ^ A string to parse as a URI template ('Burrito.parse')
  -> Values -- ^ Values to feed into the template ('Burrito.expand')
  -> Expected -- ^ The URI that should result
  -> Writer.WriterT Test m ()
test input values output = do
  let expand = Burrito.expand $ fmap (fmap unwrapValue) values
  Writer.tell
    . Test
    $ input
    Test.~: fmap expand (Burrito.parse input)
    Test.~?= expectedToMaybe output

newtype Test = Test
  { unwrapTest :: Test.Test
  } deriving (Show)

instance Monoid Test where
  mempty = Test $ Test.TestList []

instance Semigroup Test where
  x <> y = Test . Test.TestList $ case (unwrapTest x, unwrapTest y) of
    (Test.TestList t, Test.TestList u) -> t <> u
    (Test.TestList t, u) -> t <> [u]
    (t, Test.TestList u) -> t : u
    (t, u) -> [t, u]

instance Test.Testable Test where
  test = unwrapTest

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)