packages feed

burrito-1.0.2.0: src/test/Main.hs

{-# 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