packages feed

burrito 1.0.1.0 → 1.0.2.0

raw patch · 20 files changed

+687/−312 lines, 20 filesdep +QuickCheckdep +hspecdep −HUnitdep −transformersPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck, hspec

Dependencies removed: HUnit, transformers

API changes (from Hackage documentation)

+ Burrito: render :: Template -> String
+ Burrito.Expand: expand :: [(String, Value)] -> Template -> String
+ Burrito.Parse: instance GHC.Base.Alternative Burrito.Parse.Parser
+ Burrito.Parse: instance GHC.Base.Applicative Burrito.Parse.Parser
+ Burrito.Parse: instance GHC.Base.Functor Burrito.Parse.Parser
+ Burrito.Parse: instance GHC.Base.Monad Burrito.Parse.Parser
+ Burrito.Parse: parse :: String -> Maybe Template
+ Burrito.Render: render :: Template -> String
+ Burrito.TH: uriTemplate :: QuasiQuoter
+ Burrito.Type.Expression: Expression :: Operator -> NonEmpty Variable -> Expression
+ Burrito.Type.Expression: [operator] :: Expression -> Operator
+ Burrito.Type.Expression: [variables] :: Expression -> NonEmpty Variable
+ Burrito.Type.Expression: data Expression
+ Burrito.Type.Expression: instance GHC.Classes.Eq Burrito.Type.Expression.Expression
+ Burrito.Type.Expression: instance GHC.Show.Show Burrito.Type.Expression.Expression
+ Burrito.Type.Expression: instance Language.Haskell.TH.Syntax.Lift Burrito.Type.Expression.Expression
+ Burrito.Type.LitChar: Encoded :: Word8 -> LitChar
+ Burrito.Type.LitChar: Unencoded :: Char -> LitChar
+ Burrito.Type.LitChar: data LitChar
+ Burrito.Type.LitChar: instance GHC.Classes.Eq Burrito.Type.LitChar.LitChar
+ Burrito.Type.LitChar: instance GHC.Show.Show Burrito.Type.LitChar.LitChar
+ Burrito.Type.LitChar: instance Language.Haskell.TH.Syntax.Lift Burrito.Type.LitChar.LitChar
+ Burrito.Type.LitChar: isLiteral :: Char -> Bool
+ Burrito.Type.LitChar: makeUnencoded :: Char -> Maybe LitChar
+ Burrito.Type.Literal: Literal :: NonEmpty LitChar -> Literal
+ Burrito.Type.Literal: [characters] :: Literal -> NonEmpty LitChar
+ Burrito.Type.Literal: instance GHC.Classes.Eq Burrito.Type.Literal.Literal
+ Burrito.Type.Literal: instance GHC.Show.Show Burrito.Type.Literal.Literal
+ Burrito.Type.Literal: instance Language.Haskell.TH.Syntax.Lift Burrito.Type.Literal.Literal
+ Burrito.Type.Literal: newtype Literal
+ Burrito.Type.Modifier: Asterisk :: Modifier
+ Burrito.Type.Modifier: Colon :: Int -> Modifier
+ Burrito.Type.Modifier: None :: Modifier
+ Burrito.Type.Modifier: data Modifier
+ Burrito.Type.Modifier: instance GHC.Classes.Eq Burrito.Type.Modifier.Modifier
+ Burrito.Type.Modifier: instance GHC.Show.Show Burrito.Type.Modifier.Modifier
+ Burrito.Type.Modifier: instance Language.Haskell.TH.Syntax.Lift Burrito.Type.Modifier.Modifier
+ Burrito.Type.Modifier: makeColon :: Int -> Maybe Modifier
+ Burrito.Type.Name: Name :: VarChar -> [(Bool, VarChar)] -> Name
+ Burrito.Type.Name: [first] :: Name -> VarChar
+ Burrito.Type.Name: [rest] :: Name -> [(Bool, VarChar)]
+ Burrito.Type.Name: data Name
+ Burrito.Type.Name: instance GHC.Classes.Eq Burrito.Type.Name.Name
+ Burrito.Type.Name: instance GHC.Show.Show Burrito.Type.Name.Name
+ Burrito.Type.Name: instance Language.Haskell.TH.Syntax.Lift Burrito.Type.Name.Name
+ Burrito.Type.NonEmpty: NonEmpty :: a -> [a] -> NonEmpty a
+ Burrito.Type.NonEmpty: [first] :: NonEmpty a -> a
+ Burrito.Type.NonEmpty: [rest] :: NonEmpty a -> [a]
+ Burrito.Type.NonEmpty: data NonEmpty a
+ Burrito.Type.NonEmpty: fromList :: [a] -> Maybe (NonEmpty a)
+ Burrito.Type.NonEmpty: instance GHC.Classes.Eq a => GHC.Classes.Eq (Burrito.Type.NonEmpty.NonEmpty a)
+ Burrito.Type.NonEmpty: instance GHC.Show.Show a => GHC.Show.Show (Burrito.Type.NonEmpty.NonEmpty a)
+ Burrito.Type.NonEmpty: instance Language.Haskell.TH.Syntax.Lift a => Language.Haskell.TH.Syntax.Lift (Burrito.Type.NonEmpty.NonEmpty a)
+ Burrito.Type.NonEmpty: singleton :: a -> NonEmpty a
+ Burrito.Type.NonEmpty: toList :: NonEmpty a -> [a]
+ Burrito.Type.Operator: Ampersand :: Operator
+ Burrito.Type.Operator: FullStop :: Operator
+ Burrito.Type.Operator: None :: Operator
+ Burrito.Type.Operator: NumberSign :: Operator
+ Burrito.Type.Operator: PlusSign :: Operator
+ Burrito.Type.Operator: QuestionMark :: Operator
+ Burrito.Type.Operator: Semicolon :: Operator
+ Burrito.Type.Operator: Solidus :: Operator
+ Burrito.Type.Operator: data Operator
+ Burrito.Type.Operator: instance GHC.Classes.Eq Burrito.Type.Operator.Operator
+ Burrito.Type.Operator: instance GHC.Show.Show Burrito.Type.Operator.Operator
+ Burrito.Type.Operator: instance Language.Haskell.TH.Syntax.Lift Burrito.Type.Operator.Operator
+ Burrito.Type.Template: Template :: [Token] -> Template
+ Burrito.Type.Template: [tokens] :: Template -> [Token]
+ Burrito.Type.Template: instance GHC.Classes.Eq Burrito.Type.Template.Template
+ Burrito.Type.Template: instance GHC.Show.Show Burrito.Type.Template.Template
+ Burrito.Type.Template: instance Language.Haskell.TH.Syntax.Lift Burrito.Type.Template.Template
+ Burrito.Type.Template: newtype Template
+ Burrito.Type.Token: Expression :: Expression -> Token
+ Burrito.Type.Token: Literal :: Literal -> Token
+ Burrito.Type.Token: data Token
+ Burrito.Type.Token: instance GHC.Classes.Eq Burrito.Type.Token.Token
+ Burrito.Type.Token: instance GHC.Show.Show Burrito.Type.Token.Token
+ Burrito.Type.Token: instance Language.Haskell.TH.Syntax.Lift Burrito.Type.Token.Token
+ Burrito.Type.Value: Dictionary :: [(String, String)] -> Value
+ Burrito.Type.Value: List :: [String] -> Value
+ Burrito.Type.Value: String :: String -> Value
+ Burrito.Type.Value: data Value
+ Burrito.Type.Value: instance GHC.Classes.Eq Burrito.Type.Value.Value
+ Burrito.Type.Value: instance GHC.Show.Show Burrito.Type.Value.Value
+ Burrito.Type.VarChar: Encoded :: Char -> Char -> VarChar
+ Burrito.Type.VarChar: Unencoded :: Char -> VarChar
+ Burrito.Type.VarChar: data VarChar
+ Burrito.Type.VarChar: instance GHC.Classes.Eq Burrito.Type.VarChar.VarChar
+ Burrito.Type.VarChar: instance GHC.Show.Show Burrito.Type.VarChar.VarChar
+ Burrito.Type.VarChar: instance Language.Haskell.TH.Syntax.Lift Burrito.Type.VarChar.VarChar
+ Burrito.Type.VarChar: isVarchar :: Char -> Bool
+ Burrito.Type.VarChar: makeEncoded :: Char -> Char -> Maybe VarChar
+ Burrito.Type.VarChar: makeUnencoded :: Char -> Maybe VarChar
+ Burrito.Type.Variable: Variable :: Modifier -> Name -> Variable
+ Burrito.Type.Variable: [modifier] :: Variable -> Modifier
+ Burrito.Type.Variable: [name] :: Variable -> Name
+ Burrito.Type.Variable: data Variable
+ Burrito.Type.Variable: instance GHC.Classes.Eq Burrito.Type.Variable.Variable
+ Burrito.Type.Variable: instance GHC.Show.Show Burrito.Type.Variable.Variable
+ Burrito.Type.Variable: instance Language.Haskell.TH.Syntax.Lift Burrito.Type.Variable.Variable

Files

burrito.cabal view
@@ -1,5 +1,5 @@ name: burrito-version: 1.0.1.0+version: 1.0.2.0  synopsis: Parse and render URI templates. description:@@ -36,20 +36,14 @@     base >= 4.12.0 && < 4.15     , template-haskell >= 2.14.0 && < 2.17   default-language: Haskell98-  exposed-modules: Burrito-  ghc-options:-    -Weverything-    -Wno-all-missed-specialisations-    -Wno-implicit-prelude-    -Wno-missing-exported-signatures-    -Wno-safe-  hs-source-dirs: src/lib-  other-modules:+  exposed-modules:+    Burrito     Burrito.Expand     Burrito.Parse+    Burrito.Render     Burrito.TH-    Burrito.Type.Character     Burrito.Type.Expression+    Burrito.Type.LitChar     Burrito.Type.Literal     Burrito.Type.Modifier     Burrito.Type.Name@@ -58,7 +52,15 @@     Burrito.Type.Template     Burrito.Type.Token     Burrito.Type.Value+    Burrito.Type.VarChar     Burrito.Type.Variable+  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)     ghc-options:@@ -73,8 +75,8 @@   build-depends:     base -any     , burrito -any-    , HUnit >= 1.6.0 && < 1.7-    , transformers >= 0.5.6 && < 0.6+    , hspec >= 2.7.1 && < 2.8+    , QuickCheck >= 2.13.2 && < 2.14   default-language: Haskell98   hs-source-dirs: src/test   main-is: Main.hs
src/lib/Burrito.hs view
@@ -28,6 +28,7 @@ -- In short, use @parse@ to parse templates and @expand@ to render them. module Burrito   ( Parse.parse+  , Render.render   , Expand.expand   , TH.uriTemplate   , Template.Template@@ -40,6 +41,7 @@  import qualified Burrito.Expand as Expand import qualified Burrito.Parse as Parse+import qualified Burrito.Render as Render import qualified Burrito.TH as TH import qualified Burrito.Type.Template as Template import qualified Burrito.Type.Value as Value
src/lib/Burrito/Expand.hs view
@@ -1,9 +1,12 @@+-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Expand   ( expand-  ) where+  )+where -import qualified Burrito.Type.Character as Character import qualified Burrito.Type.Expression as Expression+import qualified Burrito.Type.LitChar as LitChar import qualified Burrito.Type.Literal as Literal import qualified Burrito.Type.Modifier as Modifier import qualified Burrito.Type.Name as Name@@ -12,6 +15,7 @@ import qualified Burrito.Type.Template as Template import qualified Burrito.Type.Token as Token import qualified Burrito.Type.Value as Value+import qualified Burrito.Type.VarChar as VarChar import qualified Burrito.Type.Variable as Variable import qualified Data.Bits as Bits import qualified Data.Char as Char@@ -27,26 +31,36 @@ -- appear in the output. expand :: [(String, Value.Value)] -> Template.Template -> String expand values = Identity.runIdentity-  . expandTemplate (pure . flip lookup values . nameToString)+  . expandTemplate (pure . flip lookup values . expandName)   -- | Expands a template for output according to section 3 of the RFC, using the -- given function to resolve variable values. expandTemplate-  :: Applicative m => (Name.Name -> m (Maybe Value.Value)) -> Template.Template -> m String+  :: Applicative m+  => (Name.Name -> m (Maybe Value.Value))+  -> Template.Template+  -> m String expandTemplate f = expandTokens f . Template.tokens   -- | Expands tokens for output according to section 3 of the RFC, using the -- given function to resolve variable values. expandTokens-  :: Applicative m => (Name.Name -> m (Maybe Value.Value)) -> [Token.Token] -> m String+  :: Applicative m+  => (Name.Name -> m (Maybe Value.Value))+  -> [Token.Token]+  -> m String expandTokens f = fmap concat . traverse (expandToken f)   -- | Expands a token for output according to section 3 of the RFC, using the -- given function to resolve variable values.-expandToken :: Applicative m => (Name.Name -> m (Maybe Value.Value)) -> Token.Token -> m String+expandToken+  :: Applicative m+  => (Name.Name -> m (Maybe Value.Value))+  -> Token.Token+  -> m String expandToken f token = case token of   Token.Literal literal -> pure $ expandLiteral literal   Token.Expression expression -> expandExpression f expression@@ -54,16 +68,17 @@  -- | Expands a literal token for output according to section 3.1 of the RFC. expandLiteral :: Literal.Literal -> String-expandLiteral = concatMap expandCharacter . NonEmpty.toList . Literal.characters+expandLiteral =+  concatMap expandCharacter . NonEmpty.toList . Literal.characters   -- | Expands a single literal character for output. This is necessary to -- normalize percent encodings and to encode characters that aren't allowed to -- appear in URIs.-expandCharacter :: Character.Character -> String+expandCharacter :: LitChar.LitChar -> String expandCharacter character = case character of-  Character.Encoded word8 -> percentEncodeWord8 word8-  Character.Unencoded char -> escapeChar Operator.PlusSign char+  LitChar.Encoded word8 -> percentEncodeWord8 word8+  LitChar.Unencoded char -> escapeChar Operator.PlusSign char   -- | If necessary, escapes a character for output with the given operator.@@ -97,7 +112,10 @@ -- | Expands an expression for output according to section 3.2 of the RFC, -- using the given function to resolve variable values. expandExpression-  :: Applicative m => (Name.Name -> m (Maybe Value.Value)) -> Expression.Expression -> m String+  :: Applicative m+  => (Name.Name -> m (Maybe Value.Value))+  -> Expression.Expression+  -> m String expandExpression f expression =   let     operator = Expression.operator expression@@ -165,7 +183,12 @@  -- | If the given value is not nothing, expand it according to section 3.2.1 of -- the RFC.-expandMaybeValue :: Operator.Operator -> Name.Name -> Modifier.Modifier -> Maybe Value.Value -> Maybe String+expandMaybeValue+  :: Operator.Operator+  -> Name.Name+  -> Modifier.Modifier+  -> Maybe Value.Value+  -> Maybe String expandMaybeValue operator name modifier maybeValue = do   value <- maybeValue   expandValue operator name modifier value@@ -173,7 +196,12 @@  -- | Expands a value for output according to section 3.2.1 of the RFC. If the -- value is undefined according to section 2.3, this returns nothing.-expandValue :: Operator.Operator -> Name.Name -> Modifier.Modifier -> Value.Value -> Maybe String+expandValue+  :: Operator.Operator+  -> Name.Name+  -> Modifier.Modifier+  -> Value.Value+  -> Maybe String expandValue operator name modifier value = case value of   Value.Dictionary dictionary ->     expandDictionary operator name modifier <$> NonEmpty.fromList dictionary@@ -194,7 +222,8 @@   -- | Expands one element of a dictionary value for output.-expandDictionaryElement :: Operator.Operator -> Modifier.Modifier -> (String, String) -> [String]+expandDictionaryElement+  :: Operator.Operator -> Modifier.Modifier -> (String, String) -> [String] expandDictionaryElement operator modifier (name, value) =   let escape = escapeString operator Modifier.None   in@@ -205,13 +234,18 @@  -- | Expands a list value for output. expandList-  :: Operator.Operator -> Name.Name -> Modifier.Modifier -> NonEmpty.NonEmpty String -> String+  :: Operator.Operator+  -> Name.Name+  -> Modifier.Modifier+  -> NonEmpty.NonEmpty String+  -> String expandList = expandElements $ \operator name modifier ->   pure . expandListElement operator name modifier   -- | Expands one element of a list value for output.-expandListElement :: Operator.Operator -> Name.Name -> Modifier.Modifier -> String -> String+expandListElement+  :: Operator.Operator -> Name.Name -> Modifier.Modifier -> String -> String expandListElement operator name modifier = case modifier of   Modifier.Asterisk -> expandString operator name Modifier.None   _ -> expandString Operator.None name Modifier.None@@ -235,22 +269,26 @@         Operator.QuestionMark -> True         Operator.Semicolon -> True         _ -> False-    prefix = if showPrefix then nameToString name <> "=" else ""+    prefix = if showPrefix then expandName name <> "=" else ""     separator = case modifier of       Modifier.Asterisk -> separatorFor operator       _ -> ","-  in mappend prefix . List.intercalate separator . concatMap-    (f operator name modifier) . NonEmpty.toList+  in+    mappend prefix+    . List.intercalate separator+    . concatMap (f operator name modifier)+    . NonEmpty.toList   -- | Expands a string value for output.-expandString :: Operator.Operator -> Name.Name -> Modifier.Modifier -> String -> String+expandString+  :: Operator.Operator -> Name.Name -> Modifier.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 "="+      Operator.Ampersand -> expandName name <> "="+      Operator.QuestionMark -> expandName name <> "="+      Operator.Semicolon -> expandName name <> if null s then "" else "="       _ -> ""   in prefix <> escapeString operator modifier s @@ -265,9 +303,23 @@     _ -> string  --- | Converts a name into a regular string.-nameToString :: Name.Name -> String-nameToString = NonEmpty.toList . Name.chars+-- | Expands a variable name for output.+expandName :: Name.Name -> String+expandName name = mconcat+  [ expandVarChar $ Name.first name+  , concatMap+      (\(fullStop, varChar) ->+        (if fullStop then "." else "") <> expandVarChar varChar+      )+    $ Name.rest name+  ]+++-- | Expands a single logical character of a variable name for output.+expandVarChar :: VarChar.VarChar -> String+expandVarChar varChar = case varChar of+  VarChar.Encoded hi lo -> ['%', hi, lo]+  VarChar.Unencoded char -> [char]   -- | Encodes a character as a series of UTF-8 octets. The resulting list will
src/lib/Burrito/Parse.hs view
@@ -1,9 +1,12 @@+-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Parse   ( parse-  ) where+  )+where -import qualified Burrito.Type.Character as Character import qualified Burrito.Type.Expression as Expression+import qualified Burrito.Type.LitChar as LitChar import qualified Burrito.Type.Literal as Literal import qualified Burrito.Type.Modifier as Modifier import qualified Burrito.Type.Name as Name@@ -11,6 +14,7 @@ import qualified Burrito.Type.Operator as Operator import qualified Burrito.Type.Template as Template import qualified Burrito.Type.Token as Token+import qualified Burrito.Type.VarChar as VarChar import qualified Burrito.Type.Variable as Variable import qualified Control.Applicative as Applicative import qualified Control.Monad as Monad@@ -163,20 +167,22 @@   -- | Parses a character in a literal.-parseCharacter :: Parser Character.Character+parseCharacter :: Parser LitChar.LitChar parseCharacter = parseEither parseCharacterUnencoded parseCharacterEncoded   -- | Parses an unencoded character in a literal.-parseCharacterUnencoded :: Parser Character.Character-parseCharacterUnencoded = Character.Unencoded <$> parseIf isLiteral+parseCharacterUnencoded :: Parser LitChar.LitChar+parseCharacterUnencoded = do+  char <- parseIf LitChar.isLiteral+  maybe Applicative.empty pure $ LitChar.makeUnencoded char   -- | Parses a percent-encoded character in a literal.-parseCharacterEncoded :: Parser Character.Character+parseCharacterEncoded :: Parser LitChar.LitChar parseCharacterEncoded = do   (hi, lo) <- parsePercentEncoded-  pure . Character.Encoded $ intToWord8+  pure . LitChar.Encoded $ intToWord8     (Char.digitToInt hi * 16 + Char.digitToInt lo)  @@ -199,7 +205,10 @@ parseVarspec = do   name <- parseVarname   modifier <- parseModifier-  pure $ Variable.Variable { Variable.name = name, Variable.modifier = modifier }+  pure $ Variable.Variable+    { Variable.name = name+    , Variable.modifier = modifier+    }   -- | Parses a @varname@ as defined by section 2.3 of the RFC.@@ -207,50 +216,35 @@ parseVarname = do   first <- parseVarcharFirst   rest <- Applicative.many parseVarcharRest-  pure . Name.Name $ combine first rest+  pure Name.Name { Name.first = first, Name.rest = rest }   -- | Parses the first character in a variable name, which excludes periods.-parseVarcharFirst :: Parser (NonEmpty.NonEmpty Char)+parseVarcharFirst :: Parser VarChar.VarChar parseVarcharFirst = parseEither parseVarcharUnencoded parseVarcharEncoded   -- | Parses an unencoded character in a variable name.-parseVarcharUnencoded :: Parser (NonEmpty.NonEmpty Char)-parseVarcharUnencoded = NonEmpty.singleton <$> parseIf isVarchar+parseVarcharUnencoded :: Parser VarChar.VarChar+parseVarcharUnencoded = do+  char <- parseIf VarChar.isVarchar+  maybe Applicative.empty pure $ VarChar.makeUnencoded char   -- | Parses a percent-encoded character in a variable name.-parseVarcharEncoded :: Parser (NonEmpty.NonEmpty Char)+parseVarcharEncoded :: Parser VarChar.VarChar parseVarcharEncoded = do   (hi, lo) <- parsePercentEncoded-  pure $ nonEmpty '%' [hi, lo]+  maybe Applicative.empty pure $ VarChar.makeEncoded hi lo   -- | Parses a non-first character in a variable name. This is like -- 'parseVarcharFirst' except it allows periods.-parseVarcharRest :: Parser (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.first xs)-    . mappend (NonEmpty.rest xs)-    . concatMap NonEmpty.toList+parseVarcharRest :: Parser (Bool, VarChar.VarChar)+parseVarcharRest =+  (,)+    <$> parseEither (True <$ parseChar_ '.') (pure False)+    <*> parseVarcharFirst   -- | Constructs a non-empty list without using an operator.@@ -346,7 +340,8 @@ parsePrefixModifier :: Parser Modifier.Modifier parsePrefixModifier = do   parseChar_ ':'-  Modifier.Colon <$> parseMaxLength+  maxLength <- parseMaxLength+  maybe Applicative.empty pure $ Modifier.makeColon maxLength   -- | Parses a @max-length@ as defined by section 2.4.1 of the RFC.@@ -354,18 +349,21 @@ parseMaxLength = do   first <- parseNonZeroDigit   rest <- parseUpTo 3 parseDigit-  pure . fromDigits $ nonEmpty first rest+  pure . fromDigits $ rest <> [first]  --- | 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 *)) . NonEmpty.toList+-- | Converts a backwards list of digits into the number that they represent.+-- For example @[2, 1]@ becomes @12@.+fromDigits :: [Int] -> Int+fromDigits = foldr (\digit -> (+ digit) . (* 10)) 0   -- | Parses up to the given number of occurrences of the given parser. If the -- number is less than one, this will always succeed by returning the empty -- list.+--+-- Note that for performance reasons this returns the list in reverse order. If+-- you need it in the order it was present in the input, use @reverse@. parseUpTo :: Int -> Parser a -> Parser [a] parseUpTo = parseUpToWith [] @@ -400,73 +398,6 @@ 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.Modifier parseExplodeModifier = Modifier.Asterisk <$ parseChar_ '*'----- | 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/lib/Burrito/Render.hs view
@@ -0,0 +1,121 @@+-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!.+module Burrito.Render+  ( render+  )+where++import qualified Burrito.Type.Expression as Expression+import qualified Burrito.Type.LitChar as LitChar+import qualified Burrito.Type.Literal as Literal+import qualified Burrito.Type.Modifier as Modifier+import qualified Burrito.Type.Name as Name+import qualified Burrito.Type.NonEmpty as NonEmpty+import qualified Burrito.Type.Operator as Operator+import qualified Burrito.Type.Template as Template+import qualified Burrito.Type.Token as Token+import qualified Burrito.Type.VarChar as VarChar+import qualified Burrito.Type.Variable as Variable+import qualified Data.List as List+import qualified Data.Word as Word+import qualified Text.Printf as Printf+++-- | Renders a template back into a string. This is essentially the opposite of+-- @parse@.+render :: Template.Template -> String+render = concatMap renderToken . Template.tokens+++-- | Renders a token in a template.+renderToken :: Token.Token -> String+renderToken token = case token of+  Token.Expression expression -> renderExpression expression+  Token.Literal literal -> renderLiteral literal+++-- | Renders an expression token.+renderExpression :: Expression.Expression -> String+renderExpression expression = mconcat+  [ "{"+  , renderOperator $ Expression.operator expression+  , renderVariables $ Expression.variables expression+  , "}"+  ]+++-- | Renders an operator in an expression.+renderOperator :: Operator.Operator -> String+renderOperator operator = case operator of+  Operator.Ampersand -> "&"+  Operator.FullStop -> "."+  Operator.None -> ""+  Operator.NumberSign -> "#"+  Operator.PlusSign -> "+"+  Operator.QuestionMark -> "?"+  Operator.Semicolon -> ";"+  Operator.Solidus -> "/"+++-- | Renders a bunch of variables in an expression.+renderVariables :: NonEmpty.NonEmpty Variable.Variable -> String+renderVariables = List.intercalate "," . fmap renderVariable . NonEmpty.toList+++-- | Renders a variable in an expression.+renderVariable :: Variable.Variable -> String+renderVariable variable = mconcat+  [ renderName $ Variable.name variable+  , renderModifier $ Variable.modifier variable+  ]+++-- | Renders a variable name.+renderName :: Name.Name -> String+renderName name = mconcat+  [ renderVarChar $ Name.first name+  , concatMap+      (\(fullStop, varChar) ->+        (if fullStop then "." else "") <> renderVarChar varChar+      )+    $ Name.rest name+  ]+++-- | Renders one logical character of a variable name.+renderVarChar :: VarChar.VarChar -> String+renderVarChar varChar = case varChar of+  VarChar.Encoded hi lo -> ['%', hi, lo]+  VarChar.Unencoded char -> [char]+++-- | Renders a variable modifier.+renderModifier :: Modifier.Modifier -> String+renderModifier modifier = case modifier of+  Modifier.Asterisk -> "*"+  Modifier.Colon int -> Printf.printf ":%d" int+  Modifier.None -> ""+++-- | Renders a literal token.+renderLiteral :: Literal.Literal -> String+renderLiteral =+  concatMap renderCharacter . NonEmpty.toList . Literal.characters+++-- | Renders a character in a literal token.+renderCharacter :: LitChar.LitChar -> String+renderCharacter character = case character of+  LitChar.Encoded word8 -> renderEncodedCharacter word8+  LitChar.Unencoded char -> renderUnencodedCharacter char+++-- | Renders an encoded character by percent encoding it with uppercase+-- hexadecimal digits.+renderEncodedCharacter :: Word.Word8 -> String+renderEncodedCharacter = Printf.printf "%%%02X"+++-- | Renders an unencoded character by simply turning it into a string.+renderUnencodedCharacter :: Char -> String+renderUnencodedCharacter = pure
src/lib/Burrito/TH.hs view
@@ -1,6 +1,9 @@+-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.TH   ( uriTemplate-  ) where+  )+where  import qualified Burrito.Parse as Parse import qualified Language.Haskell.TH.Quote as TH
− src/lib/Burrito/Type/Character.hs
@@ -1,17 +0,0 @@-{-# LANGUAGE DeriveLift #-}--module Burrito.Type.Character-  ( Character(..)-  ) where--import qualified Data.Word as Word-import qualified Language.Haskell.TH.Syntax as TH----- | Represents a character in a literal. Although encoded characters are--- allowed to have any value, typically they will not include most ASCII--- printable characters. In other words @A@ is more likely than @%41@.-data Character-  = Encoded Word.Word8-  | Unencoded Char-  deriving (Eq, TH.Lift, Show)
src/lib/Burrito/Type/Expression.hs view
@@ -1,8 +1,11 @@ {-# LANGUAGE DeriveLift #-} +-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Type.Expression   ( Expression(..)-  ) where+  )+where  import qualified Burrito.Type.NonEmpty as NonEmpty import qualified Burrito.Type.Operator as Operator
+ src/lib/Burrito/Type/LitChar.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE DeriveLift #-}++-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!+module Burrito.Type.LitChar+  ( LitChar(..)+  , isLiteral+  , makeUnencoded+  )+where++import qualified Data.Word as Word+import qualified Language.Haskell.TH.Syntax as TH+++-- | Represents a character in a literal. Although encoded characters are+-- allowed to have any value, typically they will not include most ASCII+-- printable characters. In other words @A@ is more likely than @%41@.+data LitChar+  = Encoded Word.Word8+  -- ^ This deliberately is not case sensitive. The tokens @%aa@ and @%AA@ are+  -- both represented as @Encoded 0xAA@.+  | Unencoded Char+  -- ^ This assumes that the character passes the @isLiteral@ predicate. You+  -- should prefer using @makeUnencoded@ to create these values.+  deriving (Eq, TH.Lift, Show)+++-- | If the character passes @isLiteral@, returns an @Unencoded@ character.+-- Otherwise returns nothing.+makeUnencoded :: Char -> Maybe LitChar+makeUnencoded char = if isLiteral char then Just $ Unencoded char else Nothing+++-- | Returns true if the given character is in the @literal@ range defined by+-- section 2.1 of the RFC.+isLiteral :: Char -> Bool+isLiteral x = case x of+  ' ' -> False+  '"' -> False+  '\'' -> False+  '%' -> False+  '<' -> False+  '>' -> False+  '\\' -> False+  '^' -> False+  '`' -> False+  '{' -> False+  '|' -> False+  '}' -> False+  _ -> between '\x20' '\x7e' x || isUcschar x || isIprivate x+++-- | Returns true if the given character is in the @ucschar@ range defined by+-- section 1.5 of the RFC.+isUcschar :: Char -> Bool+isUcschar x =+  between '\xa0' '\xd7ff' x+    || between '\xf900' '\xfdcf' x+    || between '\xfdf0' '\xffef' x+    || between '\x10000' '\x1fffd' x+    || between '\x20000' '\x2fffd' x+    || between '\x30000' '\x3fffd' x+    || between '\x40000' '\x4fffd' x+    || between '\x50000' '\x5fffd' x+    || between '\x60000' '\x6fffd' x+    || between '\x70000' '\x7fffd' x+    || between '\x80000' '\x8fffd' x+    || between '\x90000' '\x9fffd' x+    || between '\xa0000' '\xafffd' x+    || between '\xb0000' '\xbfffd' x+    || between '\xc0000' '\xcfffd' x+    || between '\xd0000' '\xdfffd' x+    || between '\xe1000' '\xefffd' x+++-- | Returns true if the given character is in the @iprivate@ range defined by+-- section 1.5 of the RFC.+isIprivate :: Char -> Bool+isIprivate x =+  between '\xe000' '\xf8ff' x+    || between '\xf0000' '\xffffd' x+    || between '\x100000' '\x10fffd' x+++-- | Returns true if the value is between the given inclusive bounds.+between+  :: Ord a+  => a -- ^ lower bound+  -> a -- ^ upper bound+  -> a+  -> Bool+between lo hi x = lo <= x && x <= hi
src/lib/Burrito/Type/Literal.hs view
@@ -1,15 +1,18 @@ {-# LANGUAGE DeriveLift #-} +-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Type.Literal   ( Literal(..)-  ) where+  )+where -import qualified Burrito.Type.Character as Character+import qualified Burrito.Type.LitChar as LitChar import qualified Burrito.Type.NonEmpty as NonEmpty import qualified Language.Haskell.TH.Syntax as TH   -- | Represents a literal in a token. newtype Literal = Literal-  { characters :: NonEmpty.NonEmpty Character.Character+  { characters :: NonEmpty.NonEmpty LitChar.LitChar   } deriving (Eq, TH.Lift, Show)
src/lib/Burrito/Type/Modifier.hs view
@@ -1,16 +1,34 @@ {-# LANGUAGE DeriveLift #-} +-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Type.Modifier   ( Modifier(..)-  ) where+  , makeColon+  )+where  import qualified Language.Haskell.TH.Syntax as TH  --- | Represents a modifier on a variable. The number associated with a prefix--- modifier will be between 1 and 9999 inclusive.+-- | Represents a modifier on a variable. data Modifier   = Asterisk   | Colon Int+  -- ^ This assumes that the number passes the @isValidMaxLength@ predicate.+  -- You should prefer using @makeColon@ to create these values.   | None   deriving (Eq, TH.Lift, Show)+++-- | If the number passes @isValidMaxLength@, returns a @Colon@ modifier.+-- Otherwise returns nothing.+makeColon :: Int -> Maybe Modifier+makeColon maxLength =+  if isValidMaxLength maxLength then Just $ Colon maxLength else Nothing+++-- | Returns true if the given number is a valid @max-length@ as defined by+-- section 2.4.1 of the RFC.+isValidMaxLength :: Int -> Bool+isValidMaxLength maxLength = 1 <= maxLength && maxLength <= 9999
src/lib/Burrito/Type/Name.hs view
@@ -1,10 +1,13 @@ {-# LANGUAGE DeriveLift #-} +-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Type.Name   ( Name(..)-  ) where+  )+where -import qualified Burrito.Type.NonEmpty as NonEmpty+import qualified Burrito.Type.VarChar as VarChar import qualified Language.Haskell.TH.Syntax as TH  @@ -12,6 +15,12 @@ -- 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-  { chars :: NonEmpty.NonEmpty Char+data Name = Name+  { first :: VarChar.VarChar+  -- ^ The first character is any valid @varchar@, including underscores and+  -- percent encoded triples.+  , rest :: [(Bool, VarChar.VarChar)]+  -- ^ Every other character has the same constraints, but they may also be+  -- preceeded by a full stop (period). That's what the @Bool@ represents:+  -- @True@ means there is a period.   } deriving (Eq, TH.Lift, Show)
src/lib/Burrito/Type/NonEmpty.hs view
@@ -1,11 +1,14 @@ {-# LANGUAGE DeriveLift #-} +-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Type.NonEmpty   ( NonEmpty(..)   , fromList   , singleton   , toList-  ) where+  )+where  import qualified Language.Haskell.TH.Syntax as TH 
src/lib/Burrito/Type/Operator.hs view
@@ -1,8 +1,11 @@ {-# LANGUAGE DeriveLift #-} +-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Type.Operator   ( Operator(..)-  ) where+  )+where  import qualified Language.Haskell.TH.Syntax as TH 
src/lib/Burrito/Type/Template.hs view
@@ -1,8 +1,11 @@ {-# LANGUAGE DeriveLift #-} +-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Type.Template   ( Template(..)-  ) where+  )+where  import qualified Burrito.Type.Token as Token import qualified Language.Haskell.TH.Syntax as TH
src/lib/Burrito/Type/Token.hs view
@@ -1,8 +1,11 @@ {-# LANGUAGE DeriveLift #-} +-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Type.Token   ( Token(..)-  ) where+  )+where  import qualified Burrito.Type.Expression as Expression import qualified Burrito.Type.Literal as Literal
src/lib/Burrito/Type/Value.hs view
@@ -1,6 +1,9 @@+-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Type.Value   ( Value(..)-  ) where+  )+where   -- | Represents a value that can be substituted into a template. Can be a
+ src/lib/Burrito/Type/VarChar.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE DeriveLift #-}++-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!.+module Burrito.Type.VarChar+  ( VarChar(..)+  , makeEncoded+  , makeUnencoded+  , isVarchar+  )+where++import qualified Data.Char as Char+import qualified Language.Haskell.TH.Syntax as TH+++-- | Represents a single logical character in a variable name.+data VarChar+  = Encoded Char Char+  -- ^ A percent encoded triple. Note that this represents three literal+  -- characters in the input, even though logically we always treat it as one+  -- character. The two arguments are the high and the low hexadecimal digits,+  -- respectively. This representation intentially keeps track of their case,+  -- so as to avoid confusing values like @%aa@ and @%AA@. You should use+  -- @makeEncoded@ to build these values.+  | Unencoded Char+  -- ^ A literal unencoded character. You should use @makeUnencoded@ to build+  -- these values.+  deriving (Eq, TH.Lift, Show)+++-- | Makes sure that both characters are valid hexadecimal digits. If they are,+-- returns an @Encoded@ character. Otherwise returns nothing.+makeEncoded :: Char -> Char -> Maybe VarChar+makeEncoded hi lo = if Char.isHexDigit hi && Char.isHexDigit lo+  then Just $ Encoded hi lo+  else Nothing+++-- | Makes sure that the character passes the @isVarchar@ predicate. If it+-- does, returns an @Unencoded@ character. Otherwise returns nothing.+makeUnencoded :: Char -> Maybe VarChar+makeUnencoded char = if isVarchar char then Just $ Unencoded char else Nothing+++-- | Returns true if the given character is in the @varchar@ range defined by+-- section 2.3 of the RFC. Note that this does not include the @pct-encoded@+-- part of the grammar because that requires multiple characters to match.+isVarchar :: Char -> Bool+isVarchar x = case x of+  '_' -> True+  _ -> isAlpha x || Char.isDigit x+++-- | Returns true if the given character is in the @ALPHA@ range defined by+-- section 1.5 of the RFC.+isAlpha :: Char -> Bool+isAlpha x = Char.isAsciiUpper x || Char.isAsciiLower x
src/lib/Burrito/Type/Variable.hs view
@@ -1,8 +1,11 @@ {-# LANGUAGE DeriveLift #-} +-- | Warning: This module is not considered part of Burrito's public API. As+-- such, it may change at any time. Use it with caution!. module Burrito.Type.Variable   ( Variable(..)-  ) where+  )+where  import qualified Burrito.Type.Modifier as Modifier import qualified Burrito.Type.Name as Name
src/test/Main.hs view
@@ -1,3 +1,5 @@+{-# OPTIONS_GHC -Wno-orphans #-}+ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}@@ -9,25 +11,36 @@ where  import qualified Burrito-import qualified Control.Monad as Monad-import qualified Control.Monad.Trans.Writer as Writer+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 System.Exit as Exit-import qualified Test.HUnit as Test+import qualified Test.Hspec as Test+import qualified Test.QuickCheck as QC  main :: IO ()-main = runTests $ do+main = Test.hspec . Test.describe "Burrito" $ do -  label "accepts empty templates" $ do+  Test.it "accepts empty templates" $ do     test "" [] "" -  label "ignores extra variables" $ do+  Test.it "ignores extra variables" $ do     test "" ["extra" =: "ignored"] "" -  label "accepts ascii literals" $ do+  Test.it "accepts ascii literals" $ do     test "!" [] "!"     test "#" [] "#"     test "$" [] "$"@@ -56,7 +69,7 @@     test "z" [] "z"     test "~" [] "~" -  label "accepts unicode literals" $ do+  Test.it "accepts unicode literals" $ do     test "\xa0" [] "%C2%A0"     test "\xd7ff" [] "%ED%9F%BF"     test "\xf900" [] "%EF%A4%80"@@ -92,7 +105,7 @@     test "\xe1000" [] "%F3%A1%80%80"     test "\xefffd" [] "%F3%AF%BF%BD" -  label "accepts private literals" $ do+  Test.it "accepts private literals" $ do     test "\xe000" [] "%EE%80%80"     test "\xf8ff" [] "%EF%A3%BF"     test "\xf0000" [] "%F3%B0%80%80"@@ -100,25 +113,25 @@     test "\x100000" [] "%F4%80%80%80"     test "\x10fffd" [] "%F4%8F%BF%BD" -  label "passes percent encoded literals through" $ do+  Test.it "passes percent encoded literals through" $ do     test "%00" [] "%00" -  label "normalizes percent encodings to uppercase" $ do+  Test.it "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.it "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.it "does not decode percent encoded literals" $ do     test "%30" [] "%30" -  label "rejects invalid literals" $ do+  Test.it "rejects invalid literals" $ do     test " " [] Failure     test "\"" [] Failure     test "'" [] Failure@@ -132,7 +145,7 @@     test "|" [] Failure     test "}" [] Failure -  label "rejects empty variable names" $ do+  Test.it "rejects empty variable names" $ do     test "{}" [] Failure     test "{,}" [] Failure     test "{a,,b}" [] Failure@@ -140,34 +153,34 @@     test "{:1}" [] Failure     test "{*}" [] Failure -  label "accepts uppercase variable names" $ do+  Test.it "accepts uppercase variable names" $ do     test "{AZ}" [] "" -  label "accepts lowercase variable names" $ do+  Test.it "accepts lowercase variable names" $ do     test "{az}" [] "" -  label "accepts decimal variable names" $ do+  Test.it "accepts decimal variable names" $ do     test "{09}" [] "" -  label "accepts underscores in variable names" $ do+  Test.it "accepts underscores in variable names" $ do     test "{_a}" [] ""     test "{a_}" [] ""     test "{_}" [] "" -  label "accepts dots in variable names" $ do+  Test.it "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.it "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.it "accepts percent encoded variable names" $ do     test "{%00}" [] ""    -- It's unclear if percent encoded triplets in variable names should be case@@ -181,40 +194,40 @@   --   -- 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.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" -  label "rejects invalid percent encoded variable names" $ do+  Test.it "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.it "rejects invalid variable names" $ do     test "{!}" [] Failure -  label "rejects invalid expressions" $ do+  Test.it "rejects invalid expressions" $ do     test "{" [] Failure     test "{{}" [] Failure     test "}" [] Failure     test "{}}" [] Failure -  label "accepts multiple variables in one expression" $ do+  Test.it "accepts multiple variables in one expression" $ do     test "{a,b}" [] ""     test "{a,b,c,d}" [] ""     test "{a,a}" [] "" -  label "accepts prefix modifiers" $ do+  Test.it "accepts prefix modifiers" $ do     test "{a:5}" [] ""     test "{a:67}" [] ""     test "{a:801}" [] ""     test "{a:234}" [] ""     test "{a:9999}" [] "" -  label "applies prefix modifiers" $ do+  Test.it "applies prefix modifiers" $ do     let values = ["a" =: "abcdefghijklmnopqrstuvwxyz"] :: Values     test "{a:1}" values "a"     test "{a:5}" values "abcde"@@ -224,23 +237,23 @@     test "{a:25}" values "abcdefghijklmnopqrstuvwxy"     test "{a:30}" values "abcdefghijklmnopqrstuvwxyz" -  label "rejects invalid prefix modifiers" $ do+  Test.it "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.it "accepts explode modifiers" $ do     test "{a*}" [] "" -  label "rejects both prefix and explode modifiers" $ do+  Test.it "rejects both prefix and explode modifiers" $ do     test "{a:1*}" [] Failure     test "{a*:1}" [] Failure -  label "accepts different modifiers on different variables" $ do+  Test.it "accepts different modifiers on different variables" $ do     test "{a,b:1,c*}" [] "" -  label "accepts allowed operators" $ do+  Test.it "accepts allowed operators" $ do     test "{+a}" [] ""     test "{#a}" [] ""     test "{.a}" [] ""@@ -249,24 +262,24 @@     test "{?a}" [] ""     test "{&a}" [] "" -  label "rejects reserved operators" $ do+  Test.it "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.it "rejects multiple operators" $ do     test "{+#a}" [] Failure -  label "rejects different operators for different variables" $ do+  Test.it "rejects different operators for different variables" $ do     test "{+a,#b}" [] Failure -  label "accepts operators and modifiers" $ do+  Test.it "accepts operators and modifiers" $ do     test "{+a:1}" [] ""     test "{#a*}" [] "" -  label "accepts multiple variables with an operator" $ do+  Test.it "accepts multiple variables with an operator" $ do     test "{+a,b}" [] ""     test "{#a,b}" [] ""     test "{.a,b}" [] ""@@ -275,24 +288,24 @@     test "{?a,b}" [] ""     test "{&a,b}" [] "" -  label "accepts multiple expressions" $ do+  Test.it "accepts multiple expressions" $ do     test "{a}{b}" [] ""     test "{a}{b}{c}{d}" [] ""     test "{a}{a}" [] "" -  label "rejects nested expressions" $ do+  Test.it "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.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" -  label "handles missing values" $ do+  Test.it "handles missing values" $ do     test "{a}" [] ""     test "{+a}" [] ""     test "{#a}" [] ""@@ -302,7 +315,7 @@     test "{?a}" [] ""     test "{&a}" [] "" -  label "handles empty list values" $ do+  Test.it "handles empty list values" $ do     let values = ["a" =: emptyList] :: Values     test "{a}" values ""     test "{+a}" values ""@@ -313,7 +326,7 @@     test "{?a}" values ""     test "{&a}" values "" -  label "handles empty dictionary values" $ do+  Test.it "handles empty dictionary values" $ do     let values = ["a" =: emptyDictionary] :: Values     test "{a}" values ""     test "{+a}" values ""@@ -324,7 +337,7 @@     test "{?a}" values ""     test "{&a}" values "" -  label "handles empty string values" $ do+  Test.it "handles empty string values" $ do     let values = ["a" =: ""] :: Values     test "{a}" values ""     test "{+a}" values ""@@ -335,7 +348,7 @@     test "{?a}" values "?a="     test "{&a}" values "&a=" -  label "handles nonempty string values" $ do+  Test.it "handles nonempty string values" $ do     let values = ["a" =: "A"] :: Values     test "{a}" values "A"     test "{+a}" values "A"@@ -346,7 +359,7 @@     test "{?a}" values "?a=A"     test "{&a}" values "&a=A" -  label "handles a mix of defined and undefined values" $ do+  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"@@ -357,7 +370,7 @@     test "{?a,b}" values "?b=B"     test "{&a,b}" values "&b=B" -  label "handles multiple empty string values" $ do+  Test.it "handles multiple empty string values" $ do     let values = ["a" =: ""] :: Values     test "{a,a}" values ","     test "{+a,a}" values ","@@ -368,7 +381,7 @@     test "{?a,a}" values "?a=&a="     test "{&a,a}" values "&a=&a=" -  label "handles multiple non-empty string values" $ do+  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"@@ -379,7 +392,7 @@     test "{?a,b}" values "?a=A&b=B"     test "{&a,b}" values "&a=A&b=B" -  label "escapes characters in composite dictionaries" $ do+  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"@@ -390,13 +403,13 @@     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.it "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+  Test.describe "passes test from rfc" $ do -    label "section 1.1" $ 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"@@ -407,7 +420,7 @@       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+    Test.describe "section 1.2" $ do       let         values =           [ "empty" =: ""@@ -420,11 +433,11 @@           , "y" =: "768"           ] :: Values -      label "level 1" $ do+      Test.it "level 1" $ do         test "{var}" values "value"         test "{hello}" values "Hello%20World%21" -      label "level 2" $ do+      Test.it "level 2" $ do         test "{+var}" values "value"         test "{+hello}" values "Hello%20World!"         test "{+path}/here" values "/foo/bar/here"@@ -432,7 +445,7 @@         test "X{#var}" values "X#value"         test "X{#hello}" values "X#Hello%20World!" -      label "level 3" $ do+      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"@@ -450,7 +463,7 @@         test "?fixed=yes{&x}" values "?fixed=yes&x=1024"         test "{&x,y,empty}" values "&x=1024&y=768&empty=" -      label "level 4" $ do+      Test.it "level 4" $ do         test "{var:3}" values "val"         test "{var:30}" values "value"         test "{list}" values "red,green,blue"@@ -494,7 +507,7 @@         test "{&keys}" values "&keys=semi,%3B,dot,.,comma,%2C"         test "{&keys*}" values "&semi=%3B&dot=.&comma=%2C" -    label "section 2.4.1" $ do+    Test.it "section 2.4.1" $ do       let values = ["var" =: "value", "semi" =: ";"] :: Values       test "{var}" values "value"       test "{var:20}" values "value"@@ -502,12 +515,14 @@       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.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" -    label "section 3.1" $ do+    Test.describe "section 3.1" $ do       let         values =           [ "base" =: "http://example.com/home/"@@ -528,7 +543,7 @@           , "y" =: "768"           ] :: Values -      label "subsection 1" $ do+      Test.it "subsection 1" $ do         test "{count}" values "one,two,three"         test "{count*}" values "one,two,three"         test "{/count}" values "/one,two,three"@@ -539,7 +554,7 @@         test "{?count*}" values "?count=one&count=two&count=three"         test "{&count*}" values "&count=one&count=two&count=three" -      label "subsection 2" $ do+      Test.it "subsection 2" $ do         test "{var}" values "value"         test "{hello}" values "Hello%20World%21"         test "{half}" values "50%25"@@ -557,7 +572,7 @@         test "{keys}" values "semi,%3B,dot,.,comma,%2C"         test "{keys*}" values "semi=%3B,dot=.,comma=%2C" -      label "subsection 3" $ do+      Test.it "subsection 3" $ do         test "{+var}" values "value"         test "{+hello}" values "Hello%20World!"         test "{+half}" values "50%25"@@ -576,7 +591,7 @@         test "{+keys}" values "semi,;,dot,.,comma,,"         test "{+keys*}" values "semi=;,dot=.,comma=," -      label "subsection 4" $ do+      Test.it "subsection 4" $ do         test "{#var}" values "#value"         test "{#hello}" values "#Hello%20World!"         test "{#half}" values "#50%25"@@ -590,7 +605,7 @@         test "{#keys}" values "#semi,;,dot,.,comma,,"         test "{#keys*}" values "#semi=;,dot=.,comma=," -      label "subsection 5" $ do+      Test.it "subsection 5" $ do         test "{.who}" values ".fred"         test "{.who,who}" values ".fred.fred"         test "{.half,who}" values ".50%25.fred"@@ -606,7 +621,7 @@         test "X{.empty_keys}" values "X"         test "X{.empty_keys*}" values "X" -      label "subsection 6" $ do+      Test.it "subsection 6" $ do         test "{/who}" values "/fred"         test "{/who,who}" values "/fred/fred"         test "{/half,who}" values "/50%25/fred"@@ -622,7 +637,7 @@         test "{/keys}" values "/semi,%3B,dot,.,comma,%2C"         test "{/keys*}" values "/semi=%3B/dot=./comma=%2C" -      label "subsection 7" $ do+      Test.it "subsection 7" $ do         test "{;who}" values ";who=fred"         test "{;half}" values ";half=50%25"         test "{;empty}" values ";empty"@@ -637,7 +652,7 @@         test "{;keys}" values ";keys=semi,%3B,dot,.,comma,%2C"         test "{;keys*}" values ";semi=%3B;dot=.;comma=%2C" -      label "subsection 8" $ do+      Test.it "subsection 8" $ do         test "{?who}" values "?who=fred"         test "{?half}" values "?half=50%25"         test "{?x,y}" values "?x=1024&y=768"@@ -649,7 +664,7 @@         test "{?keys}" values "?keys=semi,%3B,dot,.,comma,%2C"         test "{?keys*}" values "?semi=%3B&dot=.&comma=%2C" -      label "subsection 9" $ do+      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"@@ -661,7 +676,7 @@         test "{&keys}" values "&keys=semi,%3B,dot,.,comma,%2C"         test "{&keys*}" values "&semi=%3B&dot=.&comma=%2C" -  label "handles simple expansion" $ do+  Test.it "handles simple expansion" $ do     test "{a}" [] ""     test "{a}" ["a" =: emptyList] ""     test "{a}" ["a" =: emptyDictionary] ""@@ -719,7 +734,7 @@     test "{%aa*}" ["%aa" =: ["A", "B"]] "A,B"     test "{%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "A=1,B=2" -  label "handles reserved expansion" $ do+  Test.it "handles reserved expansion" $ do     test "{+a}" [] ""     test "{+a}" ["a" =: emptyList] ""     test "{+a}" ["a" =: emptyDictionary] ""@@ -777,7 +792,7 @@     test "{+%aa*}" ["%aa" =: ["A", "B"]] "A,B"     test "{+%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "A=1,B=2" -  label "handles fragment expansion" $ do+  Test.it "handles fragment expansion" $ do     test "{#a}" [] ""     test "{#a}" ["a" =: emptyList] ""     test "{#a}" ["a" =: emptyDictionary] ""@@ -835,7 +850,7 @@     test "{#%aa*}" ["%aa" =: ["A", "B"]] "#A,B"     test "{#%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "#A=1,B=2" -  label "handles label expansion" $ do+  Test.it "handles Test.it expansion" $ do     test "{.a}" [] ""     test "{.a}" ["a" =: emptyList] ""     test "{.a}" ["a" =: emptyDictionary] ""@@ -893,7 +908,7 @@     test "{.%aa*}" ["%aa" =: ["A", "B"]] ".A.B"     test "{.%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] ".A=1.B=2" -  label "handles segment expansion" $ do+  Test.it "handles segment expansion" $ do     test "{/a}" [] ""     test "{/a}" ["a" =: emptyList] ""     test "{/a}" ["a" =: emptyDictionary] ""@@ -951,7 +966,7 @@     test "{/%aa*}" ["%aa" =: ["A", "B"]] "/A/B"     test "{/%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "/A=1/B=2" -  label "handles parameter expansion" $ do+  Test.it "handles parameter expansion" $ do     test "{;a}" [] ""     test "{;a}" ["a" =: emptyList] ""     test "{;a}" ["a" =: emptyDictionary] ""@@ -1009,7 +1024,7 @@     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.it "handles query expansion" $ do     test "{?a}" [] ""     test "{?a}" ["a" =: emptyList] ""     test "{?a}" ["a" =: emptyDictionary] ""@@ -1067,7 +1082,7 @@     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.it "handles continuation expansion" $ do     test "{&a}" [] ""     test "{&a}" ["a" =: emptyList] ""     test "{&a}" ["a" =: emptyDictionary] ""@@ -1125,9 +1140,10 @@     test "{&%aa*}" ["%aa" =: ["A", "B"]] "&%aa=A&%aa=B"     test "{&%aa*}" ["%aa" =: ["A" =: "1", "B" =: "2"]] "&A=1&B=2" -  label "supports quasi quotes" $ do-    label "as expressions" $ do-      let qq x y = Writer.tell . Test $ Just x Test.~?= Burrito.parse y+  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"@@ -1144,52 +1160,14 @@       qq [Burrito.uriTemplate|{?a}|] "{?a}"       qq [Burrito.uriTemplate|{&a}|] "{&a}" -runTests :: Writer.WriterT Test IO a -> IO ()-runTests writer = do-  tests <- Writer.execWriterT writer-  counts <- Test.runTestTT $ unwrapTest tests-  let-    hasErrors = Test.errors counts /= 0-    hasFailures = Test.failures counts /= 0-  Monad.when (hasErrors || hasFailures) Exit.exitFailure--label-  :: Monad m-  => String-  -> Writer.WriterT Test m a-  -> Writer.WriterT Test m a-label string = Writer.censor $ \tests -> Test $ string Test.~: tests--test-  :: (Stack.HasCallStack, Monad m)-  => String -- ^ A string to parse as a URI template ('Burrito.parse')-  -> Values -- ^ Values to feed into the template ('Burrito.expand')-  -> Expected -- ^ The URI that should result-  -> Writer.WriterT Test m ()-test input values output = do-  let expand = Burrito.expand $ fmap (fmap unwrapValue) values-  Writer.tell-    . Test-    $ input-    Test.~: fmap expand (Burrito.parse input)-    Test.~?= expectedToMaybe output--newtype Test = Test-  { unwrapTest :: Test.Test-  } deriving (Show)--instance Monoid Test where-  mempty = Test $ Test.TestList []--instance Semigroup Test where-  x <> y = Test . Test.TestList $ case (unwrapTest x, unwrapTest y) of-    (Test.TestList t, Test.TestList u) -> t <> u-    (Test.TestList t, u) -> t <> [u]-    (t, Test.TestList u) -> t : u-    (t, u) -> [t, u]+  Test.it "round trips" . QC.property $ \template ->+    let rendered = Burrito.render $ unwrapTemplate template+    in fmap Burrito.render (Burrito.parse rendered) == Just rendered -instance Test.Testable Test where-  test = unwrapTest+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@@ -1249,3 +1227,104 @@   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