packages feed

burrito (empty) → 1.0.0.0

raw patch · 5 files changed

+2266/−0 lines, 5 filesdep +HUnitdep +basedep +burrito

Dependencies added: HUnit, base, burrito, transformers

Files

+ LICENSE.markdown view
@@ -0,0 +1,15 @@+ISC License (ISC)++Copyright 2020 Taylor Fausak++Permission to use, copy, modify, and/or distribute this software for any+purpose with or without fee is hereby granted, provided that the above+copyright notice and this permission notice appear in all copies.++THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR+PERFORMANCE OF THIS SOFTWARE.
+ README.markdown view
@@ -0,0 +1,17 @@+# Burrito++Burrito is a Haskell library for parsing and rendering URI templates.++According to [RFC 6570](https://tools.ietf.org/html/rfc6570): "A URI Template+is a compact sequence of characters for describing a range of Uniform Resource+Identifiers through variable expansion." Burrito implements URI templates+according to the specification in that RFC.++The term "uniform resource identifiers" (URI) is often used interchangeably+with other related terms like "internationalized resource identifier" (IRI),+"uniform resource locator" (URL), and "uniform resource name" (URN). Burrito+can be used for all of these. If you want to get technical, its input must be a+valid IRI and its output will be a valid URI or URN.++Although Burrito is primarily intended to be used with HTTP and HTTPS URIs, it+should work with other schemes as well.
+ burrito.cabal view
@@ -0,0 +1,59 @@+name: burrito+version: 1.0.0.0++synopsis: Parse and render URI templates.+description:+  Burrito is a Haskell library for parsing and rendering URI templates.+  .+  According to [RFC 6570](https://tools.ietf.org/html/rfc6570): "A URI Template+  is a compact sequence of characters for describing a range of Uniform+  Resource Identifiers through variable expansion." Burrito implements URI+  templates according to the specification in that RFC.+  .+  The term "uniform resource identifiers" (URI) is often used interchangeably+  with other related terms like "internationalized resource identifier" (IRI),+  "uniform resource locator" (URL), and "uniform resource name" (URN). Burrito+  can be used for all of these. If you want to get technical, its input must be+  a valid IRI and its output will be a valid URI or URN.+  .+  Although Burrito is primarily intended to be used with HTTP and HTTPS URIs,+  it should work with other schemes as well.++build-type: Simple+cabal-version: >= 1.8+category: Network+extra-source-files: README.markdown+license-file: LICENSE.markdown+license: ISC+maintainer: Taylor Fausak++library+  build-depends:+    base >= 4.12.0 && < 4.15+  exposed-modules: Burrito+  ghc-options:+    -Weverything+    -Wno-all-missed-specialisations+    -Wno-implicit-prelude+    -Wno-missing-exported-signatures+    -Wno-safe+  hs-source-dirs: src/lib++  if impl(ghc >= 8.8.1)+    ghc-options:+      -Wno-missing-deriving-strategies++  if impl(ghc >= 8.10.1)+    ghc-options:+      -Wno-missing-safe-haskell-mode+      -Wno-prepositive-qualified-module++test-suite test+  build-depends:+    base -any+    , burrito -any+    , HUnit >= 1.6.0 && < 1.7+    , transformers >= 0.5.6 && < 0.6+  hs-source-dirs: src/test+  main-is: Main.hs+  type: exitcode-stdio-1.0
+ src/lib/Burrito.hs view
@@ -0,0 +1,944 @@+-- | Burrito is a Haskell library for parsing and rendering URI templates.+--+-- According to [RFC 6570](https://tools.ietf.org/html/rfc6570): "A URI+-- Template is a compact sequence of characters for describing a range of+-- Uniform Resource Identifiers through variable expansion." Burrito implements+-- URI templates according to the specification in that RFC.+--+-- The term "uniform resource identifiers" (URI) is often used interchangeably+-- with other related terms like "internationalized resource identifier" (IRI),+-- "uniform resource locator" (URL), and "uniform resource name" (URN). Burrito+-- can be used for all of these. If you want to get technical, its input must+-- be a valid IRI and its output will be a valid URI or URN.+--+-- Although Burrito is primarily intended to be used with HTTP and HTTPS URIs,+-- it should work with other schemes as well.+--+-- If you're not already familiar with URI templates, I recommend reading the+-- overview of the RFC. It's short, to the point, and easy to understand.+--+-- Assuming you're familiar with URI templates, here's a simple example to show+-- you how Burrito works:+--+-- > import Burrito+-- > let Just template = parse "http://example.com/search{?query}"+-- > expand [ ( "query", stringValue "bikes" ) ] template+-- > "http://example.com/search?query=bikes"+--+-- In short, use 'parse' to parse templates and 'expand' to render them.+module Burrito+  ( parse+  , expand+  , Template+  , Value+  , stringValue+  , listValue+  , dictionaryValue+  )+where++import qualified Control.Applicative as Applicative+import qualified Control.Monad as Monad+import qualified Data.Bits as Bits+import qualified Data.Char as Char+import qualified Data.Functor.Identity as Identity+import qualified Data.List as List+import qualified Data.List.NonEmpty as NonEmpty+import qualified Data.Maybe as Maybe+import qualified Data.Word as Word+import qualified Text.Printf as Printf+++-- | Attempts to parse a string as a URI template. If parsing fails, this will+-- return 'Nothing'. Otherwise it will return 'Just' the parsed template.+--+-- Parsing will usually succeed, but it can fail if the input string contains+-- characters that are not valid in IRIs (like @^@) or if the input string+-- contains an invalid template expression (like @{!}@). To include characters+-- that aren't valid in IRIs, percent encode them (like @%5E@).+parse :: String -> Maybe Template+parse string = case runParser parseTemplate string of+  Just (template, "") -> Just template+  _ -> Nothing+++-- | Expands a template using the given values. Unlike parsing, expansion+-- always succeeds. If no value is given for a variable, it will simply not+-- appear in the output.+expand :: [(String, Value)] -> Template -> String+expand values = Identity.runIdentity+  . expandTemplate (pure . flip lookup values . nameToString)+++-- | Represents a URI template. Use 'parse' to create a template and 'expand'+-- to render one.+newtype Template = Template+  { template_tokens :: [Token]+  } deriving (Eq, Show)+++-- | Represents a token in a template.+data Token+  = Token_Expression Expression+  | Token_Literal Literal+  deriving (Eq, Show)+++-- | Represents a literal in a token.+newtype Literal = Literal+  { literal_characters :: NonEmpty.NonEmpty Character+  } deriving (Eq, Show)+++-- | Represents a character in a literal. Although encoded characters are+-- allowed to have any value, typically they will not include most ASCII+-- printable characters. In other words @A@ is more likely than @%41@.+data Character+  = Character_Encoded Word.Word8+  | Character_Unencoded Char+  deriving (Eq, Show)+++-- | Represents an expression in a token.+data Expression = Expression+  { expression_operator :: Operator+  , expression_variables :: NonEmpty.NonEmpty Variable+  } deriving (Eq, Show)+++-- | Represents an operator in an expression.+data Operator+  = Operator_Ampersand+  | Operator_FullStop+  | Operator_None+  | Operator_NumberSign+  | Operator_PlusSign+  | Operator_QuestionMark+  | Operator_Semicolon+  | Operator_Solidus+  deriving (Eq, Show)+++-- | Represents a variable in an expression.+data Variable = Variable+  { variable_modifier :: Modifier+  , variable_name :: Name+  } deriving (Eq, Show)+++-- | Represents a modifier on a variable. The number associated with a prefix+-- modifier will be between 1 and 9999 inclusive.+data Modifier+  = Modifier_Asterisk+  | Modifier_Colon Int+  | Modifier_None+  deriving (Eq, Show)+++-- | Represents a variable name, which is required to be non-empty. Variable+-- names allow ASCII letters and numbers, underscores, percent encoded triples,+-- and periods. However the periods cannot appear at the beginning or end, and+-- there can't be more than one of them in a row.+newtype Name = Name+  { name_chars :: NonEmpty.NonEmpty Char+  } deriving (Eq, Show)+++-- | Represents a value that can be substituted into a template. Can be a+-- string, a list, or dictionary (which is called an associative array in the+-- RFC). Use 'stringValue', 'listValue', and 'dictionaryValue' to construct+-- values.+data Value+  = Value_Dictionary [(String, String)]+  | Value_List [String]+  | Value_String String+  deriving (Eq, Show)+++-- | Constructs a string 'Value'.+stringValue :: String -> Value+stringValue = Value_String+++-- | Constructs a list 'Value'.+listValue :: [String] -> Value+listValue = Value_List+++-- | Constructs a dictionary 'Value'.+dictionaryValue :: [(String, String)] -> Value+dictionaryValue = Value_Dictionary+++-- | Expands a template for output according to section 3 of the RFC, using the+-- given function to resolve variable values.+expandTemplate+  :: Applicative m => (Name -> m (Maybe Value)) -> Template -> m String+expandTemplate f = expandTokens f . template_tokens+++-- | Expands tokens for output according to section 3 of the RFC, using the+-- given function to resolve variable values.+expandTokens+  :: Applicative m => (Name -> m (Maybe Value)) -> [Token] -> m String+expandTokens f = fmap concat . traverse (expandToken f)+++-- | Expands a token for output according to section 3 of the RFC, using the+-- given function to resolve variable values.+expandToken :: Applicative m => (Name -> m (Maybe Value)) -> Token -> m String+expandToken f token = case token of+  Token_Literal literal -> pure $ expandLiteral literal+  Token_Expression expression -> expandExpression f expression+++-- | Expands a literal token for output according to section 3.1 of the RFC.+expandLiteral :: Literal -> String+expandLiteral = concatMap expandCharacter . literal_characters+++-- | Expands a single literal character for output. This is necessary to+-- normalize percent encodings and to encode characters that aren't allowed to+-- appear in URIs.+expandCharacter :: Character -> String+expandCharacter character = case character of+  Character_Encoded word8 -> percentEncodeWord8 word8+  Character_Unencoded char -> escapeChar Operator_PlusSign char+++-- | If necessary, escapes a character for output with the given operator.+-- Otherwise returns the character unchanged as a string.+escapeChar :: Operator -> Char -> String+escapeChar operator char =+  if isAllowed operator char then [char] else percentEncodeChar char+++-- | Returns true if the given character is allowed unescaped in the output for+-- the given operator.+isAllowed :: Operator -> Char -> Bool+isAllowed operator char = case operator of+  Operator_NumberSign -> isUnreserved char || isReserved char+  Operator_PlusSign -> isUnreserved char || isReserved char+  _ -> isUnreserved char+++-- | Percent encodes a character by UTF-8 encoding it and then percent encoding+-- the resulting octets.+percentEncodeChar :: Char -> String+percentEncodeChar = concatMap percentEncodeWord8 . encodeUtf8+++-- | Percent encodes an octet by converting it into uppercase hexadecimal+-- digits and prepending a percent sign. For example @12@ becomes @"%0C"@.+percentEncodeWord8 :: Word.Word8 -> String+percentEncodeWord8 = Printf.printf "%%%02X"+++-- | Expands an expression for output according to section 3.2 of the RFC,+-- using the given function to resolve variable values.+expandExpression+  :: Applicative m => (Name -> m (Maybe Value)) -> Expression -> m String+expandExpression f expression =+  let+    operator = expression_operator expression+    prefix = prefixFor operator+    separator = separatorFor operator+    finalize expansions =+      (if null expansions then "" else prefix)+        <> List.intercalate separator expansions+  in fmap finalize . expandVariables f operator $ expression_variables+    expression+++-- | Returns the prefix to use before an expression for the given operator.+prefixFor :: Operator -> String+prefixFor operator = case operator of+  Operator_Ampersand -> "&"+  Operator_FullStop -> "."+  Operator_None -> ""+  Operator_NumberSign -> "#"+  Operator_PlusSign -> ""+  Operator_QuestionMark -> "?"+  Operator_Semicolon -> ";"+  Operator_Solidus -> "/"+++-- | Returns the separator to use between values for the given operator.+separatorFor :: Operator -> String+separatorFor operator = case operator of+  Operator_Ampersand -> "&"+  Operator_FullStop -> "."+  Operator_None -> ","+  Operator_NumberSign -> ","+  Operator_PlusSign -> ","+  Operator_QuestionMark -> "&"+  Operator_Semicolon -> ";"+  Operator_Solidus -> "/"+++-- | Expands variables for output according to section 3.2 of the RFC, using+-- the given function to resolve variable values.+expandVariables+  :: Applicative m+  => (Name -> m (Maybe Value))+  -> Operator+  -> NonEmpty.NonEmpty Variable+  -> m [String]+expandVariables f operator =+  fmap Maybe.catMaybes . traverse (expandVariable f operator) . NonEmpty.toList+++-- | Expands a variable for output according to section 3.2.1 of the RFC, using+-- the given function to resolve variable values.+expandVariable+  :: Applicative m+  => (Name -> m (Maybe Value))+  -> Operator+  -> Variable+  -> m (Maybe String)+expandVariable f operator variable =+  let+    name = variable_name variable+    modifier = variable_modifier variable+  in expandMaybeValue operator name modifier <$> f name+++-- | If the given value is not nothing, expand it according to section 3.2.1 of+-- the RFC.+expandMaybeValue :: Operator -> Name -> Modifier -> Maybe Value -> Maybe String+expandMaybeValue operator name modifier maybeValue = do+  value <- maybeValue+  expandValue operator name modifier value+++-- | Expands a value for output according to section 3.2.1 of the RFC. If the+-- value is undefined according to section 2.3, this returns nothing.+expandValue :: Operator -> Name -> Modifier -> Value -> Maybe String+expandValue operator name modifier value = case value of+  Value_Dictionary dictionary ->+    expandDictionary operator name modifier <$> NonEmpty.nonEmpty dictionary+  Value_List list ->+    expandList operator name modifier <$> NonEmpty.nonEmpty list+  Value_String string -> Just $ expandString operator name modifier string+++-- | Expands a dictionary (associative array) value for output.+expandDictionary+  :: Operator+  -> Name+  -> Modifier+  -> NonEmpty.NonEmpty (String, String)+  -> String+expandDictionary = expandElements+  $ \operator _ modifier -> expandDictionaryElement operator modifier+++-- | Expands one element of a dictionary value for output.+expandDictionaryElement :: Operator -> Modifier -> (String, String) -> [String]+expandDictionaryElement operator modifier (name, value) =+  let escape = escapeString operator Modifier_None+  in+    case modifier of+      Modifier_Asterisk -> [escape name <> "=" <> escape value]+      _ -> [escape name, escape value]+++-- | Expands a list value for output.+expandList+  :: Operator -> Name -> Modifier -> NonEmpty.NonEmpty String -> String+expandList = expandElements $ \operator name modifier ->+  pure . expandListElement operator name modifier+++-- | Expands one element of a list value for output.+expandListElement :: Operator -> Name -> Modifier -> String -> String+expandListElement operator name modifier = case modifier of+  Modifier_Asterisk -> expandString operator name Modifier_None+  _ -> expandString Operator_None name Modifier_None+++-- | Expands a collection of elements for output. This is used for both+-- dictionaries and lists.+expandElements+  :: (Operator -> Name -> Modifier -> a -> [String])+  -> Operator+  -> Name+  -> Modifier+  -> NonEmpty.NonEmpty a+  -> String+expandElements f operator name modifier =+  let+    showPrefix = case modifier of+      Modifier_Asterisk -> False+      _ -> case operator of+        Operator_Ampersand -> True+        Operator_QuestionMark -> True+        Operator_Semicolon -> True+        _ -> False+    prefix = if showPrefix then nameToString name <> "=" else ""+    separator = case modifier of+      Modifier_Asterisk -> separatorFor operator+      _ -> ","+  in mappend prefix . List.intercalate separator . concatMap+    (f operator name modifier)+++-- | Expands a string value for output.+expandString :: Operator -> Name -> Modifier -> String -> String+expandString operator name modifier s =+  let+    prefix = case operator of+      Operator_Ampersand -> nameToString name <> "="+      Operator_QuestionMark -> nameToString name <> "="+      Operator_Semicolon -> nameToString name <> if null s then "" else "="+      _ -> ""+  in prefix <> escapeString operator modifier s+++-- | Escapes a string value for output. This handles encoding characters as+-- necessary for the given oeprator, as well as taking the prefix as necessary+-- for the given modifier.+escapeString :: Operator -> Modifier -> String -> String+escapeString operator modifier string =+  concatMap (escapeChar operator) $ case modifier of+    Modifier_Colon size -> take size string+    _ -> string+++-- | Converts a name into a regular string.+nameToString :: Name -> String+nameToString = NonEmpty.toList . name_chars+++-- | Encodes a character as a series of UTF-8 octets. The resulting list will+-- have between one and four elements.+encodeUtf8 :: Char -> [Word.Word8]+encodeUtf8 char =+  let+    oneByte x = [intToWord8 $ bitAnd 0x7f x]+    twoBytes x =+      [ bitOr 0xc0 . intToWord8 . bitAnd 0x3f $ bitShiftR 6 x+      , bitOr 0x80 . intToWord8 $ bitAnd 0x3f x+      ]+    threeBytes x =+      [ bitOr 0xe0 . intToWord8 . bitAnd 0x0f $ bitShiftR 12 x+      , bitOr 0x80 . intToWord8 . bitAnd 0x3f $ bitShiftR 6 x+      , bitOr 0x80 . intToWord8 $ bitAnd 0x3f x+      ]+    fourBytes x =+      [ bitOr 0xf0 . intToWord8 . bitAnd 0x07 $ bitShiftR 18 x+      , bitOr 0x80 . intToWord8 . bitAnd 0x3f $ bitShiftR 12 x+      , bitOr 0x80 . intToWord8 . bitAnd 0x3f $ bitShiftR 6 x+      , bitOr 0x80 . intToWord8 $ bitAnd 0x3f x+      ]+  in case Char.ord char of+    int+      | int <= 0x7f -> oneByte int+      | int <= 0x7ff -> twoBytes int+      | int <= 0xffff -> threeBytes int+      | otherwise -> fourBytes int+++-- | Computes the bitwise AND of the two parameters.+bitAnd :: Bits.Bits a => a -> a -> a+bitAnd = (Bits..&.)+++-- | Computes the bitwise OR of the two parameters.+bitOr :: Bits.Bits a => a -> a -> a+bitOr = (Bits..|.)+++-- | Shifts the given value to the right by the specified number of bits. If+-- the shift amount is negative, an exception will be thrown.+bitShiftR :: Bits.Bits a => Int -> a -> a+bitShiftR = flip Bits.shiftR+++-- | Converts a machine-sized signed integer into an eight-bit unsigned+-- integer. If the input is out of bounds, an exception will be thrown.+intToWord8 :: Int -> Word.Word8+intToWord8 x =+  let+    lo = word8ToInt (minBound :: Word.Word8)+    hi = word8ToInt (maxBound :: Word.Word8)+  in if x < lo+    then error $ "intToWord8: " <> show x <> " < " <> show lo+    else if x > hi+      then error $ "intToWord8: " <> show x <> " > " <> show hi+      else fromIntegral x+++-- | Converts an eight-bit unsigned integer into a machine-sized signed+-- integer. This conversion cannot fail.+word8ToInt :: Word.Word8 -> Int+word8ToInt = fromIntegral+++-- | A simple type to handle parsing.+newtype Parser a = Parser+  { runParser :: String -> Maybe (a, String)+  }+++instance Functor Parser where+  -- | Applies the given function to the result of a successful parse.+  fmap f p = Parser $ \s -> case runParser p s of+    Nothing -> Nothing+    Just (x, t) -> Just (f x, t)+++instance Applicative Parser where+  -- | Produces a parser that always succeeds by returning the given value.+  pure x = Parser $ \s -> Just (x, s)++  -- | Uses the first parser to get a function, then uses the second parser to+  -- get a value, then calls the function with the value.+  p <*> q = Parser $ \s -> case runParser p s of+    Nothing -> Nothing+    Just (f, t) -> case runParser q t of+      Nothing -> Nothing+      Just (x, u) -> Just (f x, u)+++instance Monad Parser where+  -- | Feeds the output of a successful parse into the given function. If+  -- parsing fails, doesn't call the function.+  p >>= f = Parser $ \s -> case runParser p s of+    Nothing -> Nothing+    Just (x, t) -> runParser (f x) t+++instance Applicative.Alternative Parser where+  -- | Fails without consuming any input.+  empty = Parser $ const Nothing++  -- | Returns the first parser if it succeeds. Otherwise returns the second+  -- parser.+  p <|> q = Parser $ \s -> case runParser p s of+    Nothing -> runParser q s+    Just (x, t) -> Just (x, t)+++-- | Parses any one character. This is used as the basis for all other parsers.+parseAny :: Parser Char+parseAny = Parser $ \string -> case string of+  "" -> Nothing+  first : rest -> Just (first, rest)+++-- | Runs the given parser between the other parsers. Useful for wrapping a+-- parser in quotes or parentheses.+parseBetween :: Parser before -> Parser after -> Parser a -> Parser a+parseBetween before after parser = before *> parser <* after+++-- | Parses the given character and returns it.+parseChar :: Char -> Parser Char+parseChar = parseIf . (==)+++-- | Parses the given character and throws it away. See 'parseChar'+parseChar_ :: Char -> Parser ()+parseChar_ = Monad.void . parseChar+++-- | Tries to parse the first thing. If that fails, tries to parse the second+-- thing.+parseEither :: Parser a -> Parser a -> Parser a+parseEither = (Applicative.<|>)+++-- | Parses one character if it passes the given predicate function.+parseIf :: (Char -> Bool) -> Parser Char+parseIf predicate = do+  char <- parseAny+  if predicate char then pure char else Applicative.empty+++-- | Runs the given parser at least once.+parseNonEmpty :: Parser a -> Parser (NonEmpty.NonEmpty a)+parseNonEmpty parser = nonEmpty <$> parser <*> Applicative.many parser+++-- | Runs the given parser separated by the other parser. Requires at least one+-- occurrence of the non-separator parser.+parseSepBy1 :: Parser separator -> Parser a -> Parser (NonEmpty.NonEmpty a)+parseSepBy1 separator parser =+  nonEmpty <$> parser <*> Applicative.many (separator *> parser)+++-- | Parses a @URI-Template@ as defined by section 2 of the RFC.+parseTemplate :: Parser Template+parseTemplate = Template <$> Applicative.many parseToken+++-- | Parses a token, which we define as part of a URI template.+parseToken :: Parser Token+parseToken = parseEither+  (Token_Literal <$> parseLiteral)+  (Token_Expression <$> parseExpression)+++-- | Parses a @literals@ value as defined by section 2.1 of the RFC.+parseLiteral :: Parser Literal+parseLiteral = Literal <$> parseNonEmpty parseCharacter+++-- | Parses a character in a literal.+parseCharacter :: Parser Character+parseCharacter = parseEither parseCharacterUnencoded parseCharacterEncoded+++-- | Parses an unencoded character in a literal.+parseCharacterUnencoded :: Parser Character+parseCharacterUnencoded = Character_Unencoded <$> parseIf isLiteral+++-- | Parses a percent-encoded character in a literal.+parseCharacterEncoded :: Parser Character+parseCharacterEncoded = do+  (hi, lo) <- parsePercentEncoded+  pure . Character_Encoded $ intToWord8+    (Char.digitToInt hi * 16 + Char.digitToInt lo)+++-- | Parses an @expression@ as defined by section 2.2 of the RFC.+parseExpression :: Parser Expression+parseExpression =+  parseBetween (parseChar_ '{') (parseChar_ '}')+    $ Expression+    <$> parseOperator+    <*> parseVariableList+++-- | Parses a @variable-list@ as defined by sections 2.3 of the RFC.+parseVariableList :: Parser (NonEmpty.NonEmpty Variable)+parseVariableList = parseSepBy1 (parseChar_ ',') parseVarspec+++-- | Parses a @varspec@ as defined by section 2.3 of the RFC.+parseVarspec :: Parser Variable+parseVarspec = do+  name <- parseVarname+  modifier <- parseModifier+  pure $ Variable { variable_name = name, variable_modifier = modifier }+++-- | Parses a @varname@ as defined by section 2.3 of the RFC.+parseVarname :: Parser Name+parseVarname = do+  first <- parseVarcharFirst+  rest <- Applicative.many parseVarcharRest+  pure . Name $ combine first rest+++-- | Parses the first character in a variable name, which excludes periods.+parseVarcharFirst :: Parser (NonEmpty.NonEmpty Char)+parseVarcharFirst = parseEither parseVarcharUnencoded parseVarcharEncoded+++-- | Parses an unencoded character in a variable name.+parseVarcharUnencoded :: Parser (NonEmpty.NonEmpty Char)+parseVarcharUnencoded = pure <$> parseIf isVarchar+++-- | Parses a percent-encoded character in a variable name.+parseVarcharEncoded :: Parser (NonEmpty.NonEmpty Char)+parseVarcharEncoded = do+  (hi, lo) <- parsePercentEncoded+  pure $ nonEmpty '%' [hi, lo]+++-- | Parses a non-first character in a variable name. This is like+-- 'parseVarcharFirst' except it allows periods.+parseVarcharRest :: Parser (NonEmpty.NonEmpty Char)+parseVarcharRest = parseEither+  (nonEmpty <$> parseChar '.' <*> fmap NonEmpty.toList parseVarcharFirst)+  parseVarcharFirst+++-- | Returns true if the given character is in the @varchar@ range defined by+-- section 2.3 of the RFC. Note that this does not include the @pct-encoded@+-- part of the grammar because that requires multiple characters to match.+isVarchar :: Char -> Bool+isVarchar x = case x of+  '_' -> True+  _ -> isAlpha x || Char.isDigit x+++-- | Adds a bunch of non-empty lists to the end of one non-empty list, while+-- keeping the non-emptiness around.+combine :: NonEmpty.NonEmpty a -> [NonEmpty.NonEmpty a] -> NonEmpty.NonEmpty a+combine xs =+  nonEmpty (NonEmpty.head xs)+    . mappend (NonEmpty.tail xs)+    . concatMap NonEmpty.toList+++-- | Constructs a non-empty list without using an operator.+nonEmpty :: a -> [a] -> NonEmpty.NonEmpty a+nonEmpty = (NonEmpty.:|)+++-- | Parses a @pct-encoded@ as defined by section 1.5 of the RFC. Returns both+-- hexadecimal digits as they appeared in the input without doing any case+-- normalization.+parsePercentEncoded :: Parser (Char, Char)+parsePercentEncoded = do+  parseChar_ '%'+  (,) <$> parseIf Char.isHexDigit <*> parseIf Char.isHexDigit+++-- | Parses an @operator@ as defined by section 2.2 of the RFC.+parseOperator :: Parser Operator+parseOperator =+  Maybe.fromMaybe Operator_None <$> Applicative.optional parseRequiredOperator+++-- | Parses a required, non-reserved operator as defined by section 2.2 of the+-- RFC. See 'parseOperator'.+parseRequiredOperator :: Parser Operator+parseRequiredOperator = do+  operator <- parseIf isOperator+  maybe Applicative.empty pure $ toOperator operator+++-- | Converts an operator character into its respective 'Operator' type.+-- Returns nothing for characters that are not valid operators.+toOperator :: Char -> Maybe Operator+toOperator x = case x of+  '+' -> Just Operator_PlusSign+  '#' -> Just Operator_NumberSign+  '.' -> Just Operator_FullStop+  '/' -> Just Operator_Solidus+  ';' -> Just Operator_Semicolon+  '?' -> Just Operator_QuestionMark+  '&' -> Just Operator_Ampersand+  _ -> Nothing+++-- | Returns true if the given character is in the @operator@ range defined by+-- section 2.2 of the RFC.+isOperator :: Char -> Bool+isOperator x = isOpLevel2 x || isOpLevel3 x || isOpReserve x+++-- | Returns true if the given character is in the @op-level2@ range defined by+-- section 2.2 of the RFC.+isOpLevel2 :: Char -> Bool+isOpLevel2 x = case x of+  '+' -> True+  '#' -> True+  _ -> False+++-- | Returns true if the given character is in the @op-level3@ range defined by+-- section 2.2 of the RFC.+isOpLevel3 :: Char -> Bool+isOpLevel3 x = case x of+  '.' -> True+  '/' -> True+  ';' -> True+  '?' -> True+  '&' -> True+  _ -> False+++-- | Returns true if the given character is in the @op-reserve@ range defined+-- by section 2.2 of the RFC.+isOpReserve :: Char -> Bool+isOpReserve x = case x of+  '=' -> True+  ',' -> True+  '!' -> True+  '@' -> True+  '|' -> True+  _ -> False+++-- | Parses a @modifier-level4@ as defined by section 2.4 of the RFC.+parseModifier :: Parser Modifier+parseModifier =+  fmap (Maybe.fromMaybe Modifier_None) . Applicative.optional $ parseEither+    parsePrefixModifier+    parseExplodeModifier+++-- | Parses a @prefix@ as defined by section 2.4.1 of the RFC.+parsePrefixModifier :: Parser Modifier+parsePrefixModifier = do+  parseChar_ ':'+  Modifier_Colon <$> parseMaxLength+++-- | Parses a @max-length@ as defined by section 2.4.1 of the RFC.+parseMaxLength :: Parser Int+parseMaxLength = do+  first <- parseNonZeroDigit+  rest <- parseUpTo 3 parseDigit+  pure . fromDigits $ nonEmpty first rest+++-- | Converts a list of digits into the number that they represent. For example+-- @[1, 2]@ becomes @12@.+fromDigits :: NonEmpty.NonEmpty Int -> Int+fromDigits = foldr1 ((+) . (10 *))+++-- | Parses up to the given number of occurrences of the given parser. If the+-- number is less than one, this will always succeed by returning the empty+-- list.+parseUpTo :: Int -> Parser a -> Parser [a]+parseUpTo = parseUpToWith []+++-- | Like 'parseUpTo' but with an explicit accumulator.+parseUpToWith :: [a] -> Int -> Parser a -> Parser [a]+parseUpToWith accumulator remaining parser = if remaining < 1+  then pure accumulator+  else do+    result <- Applicative.optional parser+    case result of+      Nothing -> pure accumulator+      Just value -> parseUpToWith (value : accumulator) (remaining - 1) parser+++-- | Parses a single non-zero decimal digit and returns that digit's value. See+-- 'isNonZeroDigit'.+parseNonZeroDigit :: Parser Int+parseNonZeroDigit = Char.digitToInt <$> parseIf isNonZeroDigit+++-- | Returns true if the given character is a non-zero decimal digit. This+-- range isn't explicitly named by the RFC, but it's given in section 2.4.1.+isNonZeroDigit :: Char -> Bool+isNonZeroDigit x = case x of+  '0' -> False+  _ -> Char.isDigit x+++-- | Parses a single decimal digit and returns that digit's value.+parseDigit :: Parser Int+parseDigit = Char.digitToInt <$> parseIf Char.isDigit+++-- | Returns true if the given character is in the @ALPHA@ range defined by+-- section 1.5 of the RFC.+isAlpha :: Char -> Bool+isAlpha x = Char.isAsciiUpper x || Char.isAsciiLower x+++-- | Parses an @explode@ as defined by section 2.4.2 of the RFC.+parseExplodeModifier :: Parser Modifier+parseExplodeModifier = Modifier_Asterisk <$ parseChar_ '*'+++-- | Returns true if the given character is in the @reserved@ range defined by+-- section 1.5 of the RFC.+isReserved :: Char -> Bool+isReserved x = isGenDelim x || isSubDelim x+++-- | Returns true if the given character is in the @gen-delims@ range defined+-- by section 1.5 of the RFC.+isGenDelim :: Char -> Bool+isGenDelim x = case x of+  ':' -> True+  '/' -> True+  '?' -> True+  '#' -> True+  '[' -> True+  ']' -> True+  '@' -> True+  _ -> False+++-- | Returns true if the given character is in the @sub-delims@ range defined+-- by section 1.5 of the RFC.+isSubDelim :: Char -> Bool+isSubDelim x = case x of+  '!' -> True+  '$' -> True+  '&' -> True+  '\'' -> True+  '(' -> True+  ')' -> True+  '*' -> True+  '+' -> True+  ',' -> True+  ';' -> True+  '=' -> True+  _ -> False+++-- | Returns true if the given character is in the @unreserved@ range defined+-- by section 1.5 of the RFC.+isUnreserved :: Char -> Bool+isUnreserved x = case x of+  '-' -> True+  '.' -> True+  '_' -> True+  '~' -> True+  _ -> isAlpha x || Char.isDigit x+++-- | Returns true if the given character is in the @literal@ range defined by+-- section 2.1 of the RFC.+isLiteral :: Char -> Bool+isLiteral x = case x of+  ' ' -> False+  '"' -> False+  '\'' -> False+  '%' -> False+  '<' -> False+  '>' -> False+  '\\' -> False+  '^' -> False+  '`' -> False+  '{' -> False+  '|' -> False+  '}' -> False+  _ -> between '\x20' '\x7e' x || isUcschar x || isIprivate x+++-- | Returns true if the given character is in the @ucschar@ range defined by+-- section 1.5 of the RFC.+isUcschar :: Char -> Bool+isUcschar x =+  between '\xa0' '\xd7ff' x+    || between '\xf900' '\xfdcf' x+    || between '\xfdf0' '\xffef' x+    || between '\x10000' '\x1fffd' x+    || between '\x20000' '\x2fffd' x+    || between '\x30000' '\x3fffd' x+    || between '\x40000' '\x4fffd' x+    || between '\x50000' '\x5fffd' x+    || between '\x60000' '\x6fffd' x+    || between '\x70000' '\x7fffd' x+    || between '\x80000' '\x8fffd' x+    || between '\x90000' '\x9fffd' x+    || between '\xa0000' '\xafffd' x+    || between '\xb0000' '\xbfffd' x+    || between '\xc0000' '\xcfffd' x+    || between '\xd0000' '\xdfffd' x+    || between '\xe1000' '\xefffd' x+++-- | Returns true if the given character is in the @iprivate@ range defined by+-- section 1.5 of the RFC.+isIprivate :: Char -> Bool+isIprivate x =+  between '\xe000' '\xf8ff' x+    || between '\xf0000' '\xffffd' x+    || between '\x100000' '\x10fffd' x+++-- | Returns true if the value is between the given inclusive bounds.+between+  :: Ord a+  => a -- ^ lower bound+  -> a -- ^ upper bound+  -> a+  -> Bool+between lo hi x = lo <= x && x <= hi
+ src/test/Main.hs view
@@ -0,0 +1,1231 @@+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}+{-# 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"++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+  -> Values+  -> Expected+  -> 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)