├── cabal.project ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── CHANGELOG.md ├── .hlint.yaml ├── source ├── library │ ├── Burrito │ │ └── Internal │ │ │ ├── Type │ │ │ ├── Case.hs │ │ │ ├── MaxLength.hs │ │ │ ├── Character.hs │ │ │ ├── Modifier.hs │ │ │ ├── Operator.hs │ │ │ ├── Name.hs │ │ │ ├── Match.hs │ │ │ ├── Token.hs │ │ │ ├── Field.hs │ │ │ ├── Literal.hs │ │ │ ├── Variable.hs │ │ │ ├── Expression.hs │ │ │ ├── Value.hs │ │ │ ├── Template.hs │ │ │ └── Digit.hs │ │ │ ├── TH.hs │ │ │ ├── Render.hs │ │ │ ├── Parse.hs │ │ │ ├── Match.hs │ │ │ └── Expand.hs │ └── Burrito.hs └── test-suite │ └── Main.hs ├── LICENSE.txt ├── README.md └── burrito.cabal /cabal.project: -------------------------------------------------------------------------------- 1 | packages: . 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | /cabal.project.* 3 | /dist-newstyle/ 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | updates: 2 | - directory: / 3 | package-ecosystem: github-actions 4 | schedule: 5 | interval: weekly 6 | version: 2 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | Burrito follows the [Package Versioning Policy](https://pvp.haskell.org). 4 | You can find release notes [on GitHub](https://github.com/tfausak/burrito/releases). 5 | -------------------------------------------------------------------------------- /.hlint.yaml: -------------------------------------------------------------------------------- 1 | - group: 2 | enabled: true 3 | name: dollar 4 | - group: 5 | enabled: true 6 | name: generalise 7 | - ignore: 8 | name: Use lambda-case 9 | - ignore: 10 | name: Use tuple-section 11 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Case.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Case 4 | ( Case (..), 5 | ) 6 | where 7 | 8 | import qualified Data.Data as Data 9 | 10 | data Case 11 | = Lower 12 | | Upper 13 | deriving (Data.Data, Eq, Ord, Show) 14 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/MaxLength.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.MaxLength 4 | ( MaxLength (..), 5 | ) 6 | where 7 | 8 | import qualified Data.Data as Data 9 | 10 | newtype MaxLength = MaxLength 11 | { count :: Int 12 | } 13 | deriving (Data.Data, Eq, Ord, Show) 14 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Character.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Character 4 | ( Character (..), 5 | ) 6 | where 7 | 8 | import qualified Burrito.Internal.Type.Digit as Digit 9 | import qualified Data.Data as Data 10 | 11 | data Character tag 12 | = Encoded Digit.Digit Digit.Digit 13 | | Unencoded Char 14 | deriving (Data.Data, Eq, Ord, Show) 15 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Modifier.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Modifier 4 | ( Modifier (..), 5 | ) 6 | where 7 | 8 | import qualified Burrito.Internal.Type.MaxLength as MaxLength 9 | import qualified Data.Data as Data 10 | 11 | data Modifier 12 | = Asterisk 13 | | Colon MaxLength.MaxLength 14 | | None 15 | deriving (Data.Data, Eq, Ord, Show) 16 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Operator.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Operator 4 | ( Operator (..), 5 | ) 6 | where 7 | 8 | import qualified Data.Data as Data 9 | 10 | data Operator 11 | = Ampersand 12 | | FullStop 13 | | None 14 | | NumberSign 15 | | PlusSign 16 | | QuestionMark 17 | | Semicolon 18 | | Solidus 19 | deriving (Data.Data, Eq, Ord, Show) 20 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Name.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Name 4 | ( Name (..), 5 | ) 6 | where 7 | 8 | import qualified Burrito.Internal.Type.Field as Field 9 | import qualified Data.Data as Data 10 | import qualified Data.List.NonEmpty as NonEmpty 11 | 12 | newtype Name = Name 13 | { fields :: NonEmpty.NonEmpty Field.Field 14 | } 15 | deriving (Data.Data, Eq, Ord, Show) 16 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Match.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Match 4 | ( Match (..), 5 | ) 6 | where 7 | 8 | import qualified Burrito.Internal.Type.MaxLength as MaxLength 9 | import qualified Data.Data as Data 10 | import qualified Data.Text as Text 11 | 12 | data Match 13 | = Defined Text.Text 14 | | Prefix MaxLength.MaxLength Text.Text 15 | | Undefined 16 | deriving (Data.Data, Eq, Ord, Show) 17 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Token.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Token 4 | ( Token (..), 5 | ) 6 | where 7 | 8 | import qualified Burrito.Internal.Type.Expression as Expression 9 | import qualified Burrito.Internal.Type.Literal as Literal 10 | import qualified Data.Data as Data 11 | 12 | data Token 13 | = Expression Expression.Expression 14 | | Literal Literal.Literal 15 | deriving (Data.Data, Eq, Ord, Show) 16 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Field.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Field 4 | ( Field (..), 5 | ) 6 | where 7 | 8 | import qualified Burrito.Internal.Type.Character as Character 9 | import qualified Data.Data as Data 10 | import qualified Data.List.NonEmpty as NonEmpty 11 | 12 | newtype Field = Field 13 | { characters :: NonEmpty.NonEmpty (Character.Character Field) 14 | } 15 | deriving (Data.Data, Eq, Ord, Show) 16 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Literal.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Literal 4 | ( Literal (..), 5 | ) 6 | where 7 | 8 | import qualified Burrito.Internal.Type.Character as Character 9 | import qualified Data.Data as Data 10 | import qualified Data.List.NonEmpty as NonEmpty 11 | 12 | newtype Literal = Literal 13 | { characters :: NonEmpty.NonEmpty (Character.Character Literal) 14 | } 15 | deriving (Data.Data, Eq, Ord, Show) 16 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Variable.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Variable 4 | ( Variable (..), 5 | ) 6 | where 7 | 8 | import qualified Burrito.Internal.Type.Modifier as Modifier 9 | import qualified Burrito.Internal.Type.Name as Name 10 | import qualified Data.Data as Data 11 | 12 | data Variable = Variable 13 | { name :: Name.Name, 14 | modifier :: Modifier.Modifier 15 | } 16 | deriving (Data.Data, Eq, Ord, Show) 17 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Expression.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Expression 4 | ( Expression (..), 5 | ) 6 | where 7 | 8 | import qualified Burrito.Internal.Type.Operator as Operator 9 | import qualified Burrito.Internal.Type.Variable as Variable 10 | import qualified Data.Data as Data 11 | import qualified Data.List.NonEmpty as NonEmpty 12 | 13 | data Expression = Expression 14 | { operator :: Operator.Operator, 15 | variables :: NonEmpty.NonEmpty Variable.Variable 16 | } 17 | deriving (Data.Data, Eq, Ord, Show) 18 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Value.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Value 4 | ( Value (..), 5 | ) 6 | where 7 | 8 | import qualified Data.Data as Data 9 | import qualified Data.Map as Map 10 | import qualified Data.Text as Text 11 | 12 | -- | Represents a value that can be substituted into a template. Can be a 13 | -- string, a list, or dictionary (which is called an associative array in the 14 | -- RFC). 15 | data Value 16 | = Dictionary (Map.Map Text.Text Text.Text) 17 | | List [Text.Text] 18 | | String Text.Text 19 | deriving (Data.Data, Eq, Ord, Show) 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Taylor Fausak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Burrito 2 | 3 | [![CI](https://github.com/tfausak/burrito/actions/workflows/ci.yml/badge.svg)](https://github.com/tfausak/burrito/actions/workflows/ci.yml) 4 | [![Hackage](https://badgen.net/hackage/v/burrito)](https://hackage.haskell.org/package/burrito) 5 | 6 | Burrito is a Haskell library for parsing and rendering URI templates. 7 | 8 | According to [RFC 6570](https://tools.ietf.org/html/rfc6570): "A URI Template 9 | is a compact sequence of characters for describing a range of Uniform Resource 10 | Identifiers through variable expansion." Burrito implements URI templates 11 | according to the specification in that RFC. 12 | 13 | The term "uniform resource identifiers" (URI) is often used interchangeably 14 | with other related terms like "internationalized resource identifier" (IRI), 15 | "uniform resource locator" (URL), and "uniform resource name" (URN). Burrito 16 | can be used for all of these. If you want to get technical, its input must be a 17 | valid IRI and its output will be a valid URI or URN. 18 | 19 | Although Burrito is primarily intended to be used with HTTP and HTTPS URIs, it 20 | should work with other schemes as well. 21 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Template.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Template 4 | ( Template (..), 5 | render, 6 | ) 7 | where 8 | 9 | import qualified Burrito.Internal.Render as Render 10 | import qualified Burrito.Internal.Type.Token as Token 11 | import qualified Data.Data as Data 12 | import qualified Data.Text.Lazy.Builder as Builder 13 | 14 | -- | Represents a URI template. 15 | newtype Template = Template 16 | { tokens :: [Token.Token] 17 | } 18 | deriving (Data.Data, Eq, Ord) 19 | 20 | instance Show Template where 21 | show = render 22 | 23 | -- | Renders a template back into a string. This is essentially the opposite of 24 | -- @parse@. Usually you'll want to use @expand@ to actually substitute 25 | -- variables in the template, but this can be useful for printing out the 26 | -- template itself 27 | -- 28 | -- >>> render <$> parse "valid-template" 29 | -- Just "valid-template" 30 | -- >>> render <$> parse "{var}" 31 | -- Just "{var}" 32 | render :: Template -> String 33 | render = Render.builderToString . template 34 | 35 | template :: Template -> Builder.Builder 36 | template = foldMap Render.token . tokens 37 | -------------------------------------------------------------------------------- /source/library/Burrito.hs: -------------------------------------------------------------------------------- 1 | -- | Burrito is a Haskell library for parsing and rendering URI templates. 2 | -- 3 | -- According to [RFC 6570](https://tools.ietf.org/html/rfc6570): "A URI 4 | -- Template is a compact sequence of characters for describing a range of 5 | -- Uniform Resource Identifiers through variable expansion." Burrito 6 | -- implements URI templates according to the specification in that RFC. 7 | -- 8 | -- The term "uniform resource identifiers" (URI) is often used interchangeably 9 | -- with other related terms like "internationalized resource identifier" (IRI), 10 | -- "uniform resource locator" (URL), and "uniform resource name" (URN). Burrito 11 | -- can be used for all of these. If you want to get technical, its input must 12 | -- be a valid IRI and its output will be a valid URI or URN. 13 | -- 14 | -- Although Burrito is primarily intended to be used with HTTP and HTTPS URIs, 15 | -- it should work with other schemes as well. 16 | -- 17 | -- If you're not already familiar with URI templates, I recommend reading the 18 | -- overview of the RFC. It's short, to the point, and easy to understand. 19 | -- 20 | -- Assuming you're familiar with URI templates, here's a simple example to show 21 | -- you how Burrito works: 22 | -- 23 | -- >>> import Burrito 24 | -- >>> let Just template = parse "http://example/search{?query}" 25 | -- >>> expand [("query", stringValue "chorizo")] template 26 | -- "http://example.com/search?query=chorizo" 27 | -- 28 | -- In short, use @parse@ to parse templates and @expand@ to render them. 29 | module Burrito 30 | ( Parse.parse, 31 | Template.render, 32 | Expand.expand, 33 | Expand.expandWith, 34 | Match.match, 35 | TH.uriTemplate, 36 | TH.expandTH, 37 | Template.Template, 38 | Value.Value, 39 | stringValue, 40 | listValue, 41 | dictionaryValue, 42 | ) 43 | where 44 | 45 | import qualified Burrito.Internal.Expand as Expand 46 | import qualified Burrito.Internal.Match as Match 47 | import qualified Burrito.Internal.Parse as Parse 48 | import qualified Burrito.Internal.TH as TH 49 | import qualified Burrito.Internal.Type.Template as Template 50 | import qualified Burrito.Internal.Type.Value as Value 51 | import qualified Data.Bifunctor as Bifunctor 52 | import qualified Data.Map as Map 53 | import qualified Data.Text as Text 54 | 55 | -- | Constructs a string value. 56 | stringValue :: String -> Value.Value 57 | stringValue = Value.String . Text.pack 58 | 59 | -- | Constructs a list value. 60 | listValue :: [String] -> Value.Value 61 | listValue = Value.List . fmap Text.pack 62 | 63 | -- | Constructs a dictionary value. 64 | dictionaryValue :: [(String, String)] -> Value.Value 65 | dictionaryValue = 66 | Value.Dictionary . Map.fromList . fmap (Bifunctor.bimap Text.pack Text.pack) 67 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Type/Digit.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | module Burrito.Internal.Type.Digit 4 | ( Digit (..), 5 | fromChar, 6 | fromWord8, 7 | toWord8, 8 | ) 9 | where 10 | 11 | import qualified Burrito.Internal.Type.Case as Case 12 | import qualified Data.Bits as Bits 13 | import qualified Data.Data as Data 14 | import qualified Data.Word as Word 15 | 16 | data Digit 17 | = Ox0 18 | | Ox1 19 | | Ox2 20 | | Ox3 21 | | Ox4 22 | | Ox5 23 | | Ox6 24 | | Ox7 25 | | Ox8 26 | | Ox9 27 | | OxA Case.Case 28 | | OxB Case.Case 29 | | OxC Case.Case 30 | | OxD Case.Case 31 | | OxE Case.Case 32 | | OxF Case.Case 33 | deriving (Data.Data, Eq, Ord, Show) 34 | 35 | fromChar :: Char -> Maybe Digit 36 | fromChar x = case x of 37 | '0' -> Just Ox0 38 | '1' -> Just Ox1 39 | '2' -> Just Ox2 40 | '3' -> Just Ox3 41 | '4' -> Just Ox4 42 | '5' -> Just Ox5 43 | '6' -> Just Ox6 44 | '7' -> Just Ox7 45 | '8' -> Just Ox8 46 | '9' -> Just Ox9 47 | 'A' -> Just $ OxA Case.Upper 48 | 'B' -> Just $ OxB Case.Upper 49 | 'C' -> Just $ OxC Case.Upper 50 | 'D' -> Just $ OxD Case.Upper 51 | 'E' -> Just $ OxE Case.Upper 52 | 'F' -> Just $ OxF Case.Upper 53 | 'a' -> Just $ OxA Case.Lower 54 | 'b' -> Just $ OxB Case.Lower 55 | 'c' -> Just $ OxC Case.Lower 56 | 'd' -> Just $ OxD Case.Lower 57 | 'e' -> Just $ OxE Case.Lower 58 | 'f' -> Just $ OxF Case.Lower 59 | _ -> Nothing 60 | 61 | fromWord8 :: Word.Word8 -> (Digit, Digit) 62 | fromWord8 x = 63 | let f :: Word.Word8 -> Digit 64 | f y = case y of 65 | 0x0 -> Ox0 66 | 0x1 -> Ox1 67 | 0x2 -> Ox2 68 | 0x3 -> Ox3 69 | 0x4 -> Ox4 70 | 0x5 -> Ox5 71 | 0x6 -> Ox6 72 | 0x7 -> Ox7 73 | 0x8 -> Ox8 74 | 0x9 -> Ox9 75 | 0xA -> OxA Case.Upper 76 | 0xB -> OxB Case.Upper 77 | 0xC -> OxC Case.Upper 78 | 0xD -> OxD Case.Upper 79 | 0xE -> OxE Case.Upper 80 | 0xF -> OxF Case.Upper 81 | _ -> error $ "invalid nibble: " <> show y 82 | in (f $ Bits.shiftR x 4, f $ x Bits..&. 0x0F) 83 | 84 | toWord8 :: Digit -> Digit -> Word.Word8 85 | toWord8 x y = 86 | let f :: Digit -> Word.Word8 87 | f z = case z of 88 | Ox0 -> 0x0 89 | Ox1 -> 0x1 90 | Ox2 -> 0x2 91 | Ox3 -> 0x3 92 | Ox4 -> 0x4 93 | Ox5 -> 0x5 94 | Ox6 -> 0x6 95 | Ox7 -> 0x7 96 | Ox8 -> 0x8 97 | Ox9 -> 0x9 98 | OxA _ -> 0xA 99 | OxB _ -> 0xB 100 | OxC _ -> 0xC 101 | OxD _ -> 0xD 102 | OxE _ -> 0xE 103 | OxF _ -> 0xF 104 | in Bits.shiftL (f x) 4 Bits..|. f y 105 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/TH.hs: -------------------------------------------------------------------------------- 1 | module Burrito.Internal.TH 2 | ( expandTH, 3 | uriTemplate, 4 | ) 5 | where 6 | 7 | import qualified Burrito.Internal.Expand as Expand 8 | import qualified Burrito.Internal.Parse as Parse 9 | import qualified Burrito.Internal.Type.Template as Template 10 | import qualified Burrito.Internal.Type.Value as Value 11 | import qualified Data.Bifunctor as Bifunctor 12 | import qualified Data.Map as Map 13 | import qualified Data.Text as Text 14 | import qualified Data.Text.Lazy as LazyText 15 | import qualified Data.Text.Lazy.Builder as Builder 16 | import qualified Language.Haskell.TH.Quote as TH 17 | import qualified Language.Haskell.TH.Syntax as TH 18 | 19 | -- | This can be used together with the @TemplateHaskell@ language extension to 20 | -- expand a URI template at compile time. This is slightly different from 21 | -- 'Expand.expand' in that missing variables will throw an exception. This is 22 | -- convenient because it allows you to verify that all of the variables have 23 | -- been supplied at compile time. 24 | -- 25 | -- >>> :set -XQuasiQuotes -XTemplateHaskell 26 | -- >>> import Burrito 27 | -- >>> $( expandTH [("foo", stringValue "bar")] [uriTemplate|l-{foo}-r|] ) 28 | -- "l-bar-r" 29 | expandTH :: [(String, Value.Value)] -> Template.Template -> TH.Q TH.Exp 30 | expandTH xs t = do 31 | let m = Map.fromList $ fmap (Bifunctor.first Text.pack) xs 32 | f k = 33 | maybe (Left $ "missing variable: " <> show k) (Right . Just) $ 34 | Map.lookup k m 35 | x <- 36 | either fail (pure . LazyText.unpack . Builder.toLazyText) $ 37 | Expand.expandWith f t 38 | TH.lift x 39 | 40 | -- | This can be used together with the @QuasiQuotes@ language extension to 41 | -- parse a URI template at compile time. This is convenient because it allows 42 | -- you to verify the validity of the template when you compile your file as 43 | -- opposed to when you run it. 44 | -- 45 | -- >>> :set -XQuasiQuotes 46 | -- >>> import Burrito 47 | -- >>> let template = [uriTemplate|http://example/search{?query}|] 48 | -- >>> let values = [("query", stringValue "chorizo")] 49 | -- >>> expand values template 50 | -- "http://example/search?query=chorizo" 51 | -- 52 | -- Note that you cannot use escape sequences in this quasi-quoter. For example, 53 | -- this is invalid: @[uriTemplate|\\xa0|]@. You can however use percent encoded 54 | -- triples as normal. So this is valid: @[uriTemplate|%c2%a0|]@. 55 | uriTemplate :: TH.QuasiQuoter 56 | uriTemplate = 57 | TH.QuasiQuoter 58 | { TH.quoteDec = const $ fail "cannot be used as a declaration", 59 | TH.quoteExp = maybe (fail "invalid URI template") TH.liftData . Parse.parse, 60 | TH.quotePat = const $ fail "cannot be used as a pattern", 61 | TH.quoteType = const $ fail "cannot be used as a type" 62 | } 63 | -------------------------------------------------------------------------------- /burrito.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: burrito 3 | version: 2.0.1.14 4 | synopsis: Parse and render URI templates. 5 | description: 6 | Burrito is a Haskell library for parsing and rendering URI templates. 7 | . 8 | According to [RFC 6570](https://tools.ietf.org/html/rfc6570): "A URI Template 9 | is a compact sequence of characters for describing a range of Uniform 10 | Resource Identifiers through variable expansion." Burrito implements URI 11 | templates according to the specification in that RFC. 12 | . 13 | The term "uniform resource identifiers" (URI) is often used interchangeably 14 | with other related terms like "internationalized resource identifier" (IRI), 15 | "uniform resource locator" (URL), and "uniform resource name" (URN). Burrito 16 | can be used for all of these. If you want to get technical, its input must be 17 | a valid IRI and its output will be a valid URI or URN. 18 | . 19 | Although Burrito is primarily intended to be used with HTTP and HTTPS URIs, 20 | it should work with other schemes as well. 21 | 22 | build-type: Simple 23 | category: Network 24 | extra-doc-files: 25 | CHANGELOG.md 26 | README.md 27 | 28 | license-file: LICENSE.txt 29 | license: MIT 30 | maintainer: Taylor Fausak 31 | 32 | source-repository head 33 | location: https://github.com/tfausak/burrito 34 | type: git 35 | 36 | flag pedantic 37 | default: False 38 | manual: True 39 | 40 | common library 41 | build-depends: base ^>=4.19.0.0 || ^>=4.20.0.0 || ^>=4.21.0.0 42 | default-language: Haskell2010 43 | ghc-options: 44 | -Weverything 45 | -Wno-all-missed-specialisations 46 | -Wno-implicit-prelude 47 | -Wno-missed-specialisations 48 | -Wno-missing-deriving-strategies 49 | -Wno-missing-exported-signatures 50 | -Wno-missing-kind-signatures 51 | -Wno-missing-role-annotations 52 | -Wno-missing-safe-haskell-mode 53 | -Wno-prepositive-qualified-module 54 | -Wno-safe 55 | -Wno-unsafe 56 | 57 | if flag(pedantic) 58 | ghc-options: -Werror 59 | 60 | common executable 61 | import: library 62 | build-depends: burrito 63 | ghc-options: 64 | -rtsopts 65 | -threaded 66 | 67 | library 68 | import: library 69 | build-depends: 70 | bytestring ^>=0.11.4.0 || ^>=0.12.0.2, 71 | containers ^>=0.6.7 || ^>=0.7, 72 | parsec ^>=3.1.16.1, 73 | template-haskell ^>=2.21.0.0 || ^>=2.22.0.0 || ^>=2.23.0.0, 74 | text ^>=2.0.2 || ^>=2.1, 75 | transformers ^>=0.6.1.0, 76 | 77 | -- cabal-gild: discover source/library 78 | exposed-modules: 79 | Burrito 80 | Burrito.Internal.Expand 81 | Burrito.Internal.Match 82 | Burrito.Internal.Parse 83 | Burrito.Internal.Render 84 | Burrito.Internal.TH 85 | Burrito.Internal.Type.Case 86 | Burrito.Internal.Type.Character 87 | Burrito.Internal.Type.Digit 88 | Burrito.Internal.Type.Expression 89 | Burrito.Internal.Type.Field 90 | Burrito.Internal.Type.Literal 91 | Burrito.Internal.Type.Match 92 | Burrito.Internal.Type.MaxLength 93 | Burrito.Internal.Type.Modifier 94 | Burrito.Internal.Type.Name 95 | Burrito.Internal.Type.Operator 96 | Burrito.Internal.Type.Template 97 | Burrito.Internal.Type.Token 98 | Burrito.Internal.Type.Value 99 | Burrito.Internal.Type.Variable 100 | 101 | hs-source-dirs: source/library 102 | 103 | test-suite burrito-test-suite 104 | import: executable 105 | build-depends: 106 | QuickCheck ^>=2.14.3 || ^>=2.15 || ^>=2.16, 107 | containers, 108 | hspec ^>=2.11.8, 109 | text, 110 | 111 | hs-source-dirs: source/test-suite 112 | main-is: Main.hs 113 | type: exitcode-stdio-1.0 114 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | build: 3 | name: GHC ${{ matrix.ghc }} on ${{ matrix.os }} 4 | runs-on: ${{ matrix.os }} 5 | steps: 6 | - uses: actions/checkout@v6 7 | - run: mkdir artifact 8 | - uses: haskell/ghcup-setup@v1 9 | with: 10 | ghc: ${{ matrix.ghc }} 11 | cabal: latest 12 | - run: ghc-pkg list 13 | - run: cabal sdist --output-dir artifact 14 | - run: cabal configure --enable-documentation --enable-tests --flags=pedantic --haddock-for-hackage --jobs 15 | - run: cat cabal.project.local 16 | - run: cp cabal.project.local artifact 17 | - run: cabal update 18 | - run: cabal freeze 19 | - run: cat cabal.project.freeze 20 | - run: cp cabal.project.freeze artifact 21 | - run: cabal outdated --v2-freeze-file 22 | - uses: actions/cache@v5 23 | with: 24 | key: ${{ matrix.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} 25 | path: ~/.local/state/cabal 26 | restore-keys: ${{ matrix.os }}-${{ matrix.ghc }}- 27 | - run: cabal build --only-download 28 | - run: cabal build --only-dependencies 29 | - run: cabal build 30 | - run: cp dist-newstyle/*-docs.tar.gz artifact 31 | - run: tar --create --file artifact.tar --verbose artifact 32 | - uses: actions/upload-artifact@v6 33 | with: 34 | name: burrito-${{ github.sha }}-${{ matrix.os }}-${{ matrix.ghc }} 35 | path: artifact.tar 36 | - run: cabal run -- burrito-test-suite --randomize --strict 37 | strategy: 38 | matrix: 39 | include: 40 | - ghc: 9.12 41 | os: macos-14 42 | - ghc: 9.8 43 | os: ubuntu-24.04 44 | - ghc: '9.10' 45 | os: ubuntu-24.04 46 | - ghc: 9.12 47 | os: ubuntu-24.04 48 | - ghc: 9.12 49 | os: windows-2022 50 | cabal: 51 | name: Cabal 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v6 55 | - run: cabal check 56 | gild: 57 | name: Gild 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v6 61 | - uses: tfausak/cabal-gild-setup-action@v2 62 | - run: cabal-gild --input burrito.cabal --mode check 63 | hlint: 64 | name: HLint 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v6 68 | - uses: haskell-actions/hlint-setup@v2 69 | - uses: haskell-actions/hlint-run@v2 70 | with: 71 | fail-on: status 72 | ormolu: 73 | name: Ormolu 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v6 77 | - uses: haskell-actions/run-ormolu@v17 78 | release: 79 | if: ${{ github.event_name == 'release' }} 80 | name: Release 81 | needs: build 82 | permissions: 83 | contents: write 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/download-artifact@v7 87 | with: 88 | name: burrito-${{ github.sha }}-ubuntu-24.04-9.12 89 | - run: tar --extract --file artifact.tar --verbose 90 | - uses: softprops/action-gh-release@v2 91 | with: 92 | files: artifact/burrito-${{ github.event.release.tag_name }}.tar.gz 93 | - run: cabal upload --publish --username '${{ secrets.HACKAGE_USERNAME }}' --password '${{ secrets.HACKAGE_PASSWORD }}' artifact/burrito-${{ github.event.release.tag_name }}.tar.gz 94 | - run: cabal --http-transport=plain-http upload --documentation --publish --username '${{ secrets.HACKAGE_USERNAME }}' --password '${{ secrets.HACKAGE_PASSWORD }}' artifact/burrito-${{ github.event.release.tag_name }}-docs.tar.gz 95 | name: CI 96 | on: 97 | pull_request: 98 | branches: 99 | - main 100 | push: 101 | branches: 102 | - main 103 | release: 104 | types: 105 | - created 106 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Render.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-missing-export-lists #-} 2 | 3 | module Burrito.Internal.Render where 4 | 5 | import qualified Burrito.Internal.Type.Case as Case 6 | import qualified Burrito.Internal.Type.Character as Character 7 | import qualified Burrito.Internal.Type.Digit as Digit 8 | import qualified Burrito.Internal.Type.Expression as Expression 9 | import qualified Burrito.Internal.Type.Field as Field 10 | import qualified Burrito.Internal.Type.Literal as Literal 11 | import qualified Burrito.Internal.Type.MaxLength as MaxLength 12 | import qualified Burrito.Internal.Type.Modifier as Modifier 13 | import qualified Burrito.Internal.Type.Name as Name 14 | import qualified Burrito.Internal.Type.Operator as Operator 15 | import qualified Burrito.Internal.Type.Token as Token 16 | import qualified Burrito.Internal.Type.Variable as Variable 17 | import qualified Data.List as List 18 | import qualified Data.List.NonEmpty as NonEmpty 19 | import qualified Data.Text.Lazy as LazyText 20 | import qualified Data.Text.Lazy.Builder as Builder 21 | 22 | builderToString :: Builder.Builder -> String 23 | builderToString = LazyText.unpack . Builder.toLazyText 24 | 25 | token :: Token.Token -> Builder.Builder 26 | token x = case x of 27 | Token.Expression y -> expression y 28 | Token.Literal y -> literal y 29 | 30 | expression :: Expression.Expression -> Builder.Builder 31 | expression x = 32 | Builder.singleton '{' 33 | <> operator (Expression.operator x) 34 | <> sepBy1 variable (Builder.singleton ',') (Expression.variables x) 35 | <> Builder.singleton '}' 36 | 37 | operator :: Operator.Operator -> Builder.Builder 38 | operator x = case x of 39 | Operator.Ampersand -> Builder.singleton '&' 40 | Operator.FullStop -> Builder.singleton '.' 41 | Operator.None -> mempty 42 | Operator.NumberSign -> Builder.singleton '#' 43 | Operator.PlusSign -> Builder.singleton '+' 44 | Operator.QuestionMark -> Builder.singleton '?' 45 | Operator.Semicolon -> Builder.singleton ';' 46 | Operator.Solidus -> Builder.singleton '/' 47 | 48 | sepBy1 :: 49 | (a -> Builder.Builder) -> 50 | Builder.Builder -> 51 | NonEmpty.NonEmpty a -> 52 | Builder.Builder 53 | sepBy1 f x = mconcat . List.intersperse x . fmap f . NonEmpty.toList 54 | 55 | variable :: Variable.Variable -> Builder.Builder 56 | variable x = name (Variable.name x) <> modifier (Variable.modifier x) 57 | 58 | name :: Name.Name -> Builder.Builder 59 | name = sepBy1 field (Builder.singleton '.') . Name.fields 60 | 61 | field :: Field.Field -> Builder.Builder 62 | field = foldMap character . Field.characters 63 | 64 | character :: Character.Character tag -> Builder.Builder 65 | character x = case x of 66 | Character.Encoded y z -> encodedCharacter y z 67 | Character.Unencoded y -> Builder.singleton y 68 | 69 | encodedCharacter :: Digit.Digit -> Digit.Digit -> Builder.Builder 70 | encodedCharacter x y = Builder.singleton '%' <> digit x <> digit y 71 | 72 | digit :: Digit.Digit -> Builder.Builder 73 | digit x = Builder.singleton $ case x of 74 | Digit.Ox0 -> '0' 75 | Digit.Ox1 -> '1' 76 | Digit.Ox2 -> '2' 77 | Digit.Ox3 -> '3' 78 | Digit.Ox4 -> '4' 79 | Digit.Ox5 -> '5' 80 | Digit.Ox6 -> '6' 81 | Digit.Ox7 -> '7' 82 | Digit.Ox8 -> '8' 83 | Digit.Ox9 -> '9' 84 | Digit.OxA Case.Upper -> 'A' 85 | Digit.OxB Case.Upper -> 'B' 86 | Digit.OxC Case.Upper -> 'C' 87 | Digit.OxD Case.Upper -> 'D' 88 | Digit.OxE Case.Upper -> 'E' 89 | Digit.OxF Case.Upper -> 'F' 90 | Digit.OxA Case.Lower -> 'a' 91 | Digit.OxB Case.Lower -> 'b' 92 | Digit.OxC Case.Lower -> 'c' 93 | Digit.OxD Case.Lower -> 'd' 94 | Digit.OxE Case.Lower -> 'e' 95 | Digit.OxF Case.Lower -> 'f' 96 | 97 | modifier :: Modifier.Modifier -> Builder.Builder 98 | modifier x = case x of 99 | Modifier.Asterisk -> Builder.singleton '*' 100 | Modifier.Colon y -> Builder.singleton ':' <> maxLength y 101 | Modifier.None -> mempty 102 | 103 | maxLength :: MaxLength.MaxLength -> Builder.Builder 104 | maxLength = Builder.fromString . show . MaxLength.count 105 | 106 | literal :: Literal.Literal -> Builder.Builder 107 | literal = foldMap character . Literal.characters 108 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Parse.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# OPTIONS_GHC -Wno-missing-export-lists #-} 3 | 4 | module Burrito.Internal.Parse where 5 | 6 | import qualified Burrito.Internal.Type.Character as Character 7 | import qualified Burrito.Internal.Type.Digit as Digit 8 | import qualified Burrito.Internal.Type.Expression as Expression 9 | import qualified Burrito.Internal.Type.Field as Field 10 | import qualified Burrito.Internal.Type.Literal as Literal 11 | import qualified Burrito.Internal.Type.MaxLength as MaxLength 12 | import qualified Burrito.Internal.Type.Modifier as Modifier 13 | import qualified Burrito.Internal.Type.Name as Name 14 | import qualified Burrito.Internal.Type.Operator as Operator 15 | import qualified Burrito.Internal.Type.Template as Template 16 | import qualified Burrito.Internal.Type.Token as Token 17 | import qualified Burrito.Internal.Type.Variable as Variable 18 | import qualified Data.Char as Char 19 | import qualified Data.Ix as Ix 20 | import qualified Data.List.NonEmpty as NonEmpty 21 | import qualified Text.Parsec as Parsec 22 | import qualified Text.Read as Read 23 | 24 | -- | Attempts to parse a string as a URI template. If parsing fails, this will 25 | -- return @Nothing@. Otherwise it will return @Just@ the parsed template. 26 | -- 27 | -- Parsing will usually succeed, but it can fail if the input string contains 28 | -- characters that are not valid in IRIs (like @^@) or if the input string 29 | -- contains an invalid template expression (like @{!}@). To include characters 30 | -- that aren't valid in IRIs, percent encode them (like @%5E@). 31 | -- 32 | -- >>> parse "invalid template" 33 | -- Nothing 34 | -- >>> parse "valid-template" 35 | -- Just (Template ...) 36 | parse :: String -> Maybe Template.Template 37 | parse = either (const Nothing) Just . Parsec.parse template "" 38 | 39 | template :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Template.Template 40 | template = Template.Template <$> Parsec.many token <* Parsec.eof 41 | 42 | token :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Token.Token 43 | token = choice (Token.Expression <$> expression) (Token.Literal <$> literal) 44 | 45 | choice :: 46 | Parsec.ParsecT s u m a -> Parsec.ParsecT s u m a -> Parsec.ParsecT s u m a 47 | choice = (Parsec.<|>) 48 | 49 | expression :: 50 | (Parsec.Stream s m Char) => Parsec.ParsecT s u m Expression.Expression 51 | expression = 52 | Parsec.between (Parsec.char '{') (Parsec.char '}') $ 53 | Expression.Expression 54 | <$> operator 55 | <*> sepBy1 variable (Parsec.char ',') 56 | 57 | operator :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Operator.Operator 58 | operator = 59 | Parsec.option Operator.None $ 60 | Parsec.choice 61 | [ Operator.Ampersand <$ Parsec.char '&', 62 | Operator.FullStop <$ Parsec.char '.', 63 | Operator.NumberSign <$ Parsec.char '#', 64 | Operator.PlusSign <$ Parsec.char '+', 65 | Operator.QuestionMark <$ Parsec.char '?', 66 | Operator.Semicolon <$ Parsec.char ';', 67 | Operator.Solidus <$ Parsec.char '/' 68 | ] 69 | 70 | sepBy1 :: 71 | Parsec.ParsecT s u m a -> 72 | Parsec.ParsecT s u m x -> 73 | Parsec.ParsecT s u m (NonEmpty.NonEmpty a) 74 | sepBy1 p s = (NonEmpty.:|) <$> p <*> Parsec.many (s *> p) 75 | 76 | variable :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Variable.Variable 77 | variable = Variable.Variable <$> name <*> modifier 78 | 79 | name :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Name.Name 80 | name = Name.Name <$> sepBy1 field (Parsec.char '.') 81 | 82 | field :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Field.Field 83 | field = Field.Field <$> nonEmpty fieldCharacter 84 | 85 | nonEmpty :: 86 | Parsec.ParsecT s u m a -> Parsec.ParsecT s u m (NonEmpty.NonEmpty a) 87 | nonEmpty p = (NonEmpty.:|) <$> p <*> Parsec.many p 88 | 89 | fieldCharacter :: 90 | (Parsec.Stream s m Char) => 91 | Parsec.ParsecT s u m (Character.Character Field.Field) 92 | fieldCharacter = choice encodedCharacter (unencodedCharacter isFieldCharacter) 93 | 94 | encodedCharacter :: 95 | (Parsec.Stream s m Char) => Parsec.ParsecT s u m (Character.Character tag) 96 | encodedCharacter = Parsec.char '%' >> Character.Encoded <$> digit <*> digit 97 | 98 | digit :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Digit.Digit 99 | digit = do 100 | x <- Parsec.satisfy Char.isHexDigit 101 | maybe (fail "invalid Digit") pure $ Digit.fromChar x 102 | 103 | unencodedCharacter :: 104 | (Parsec.Stream s m Char) => 105 | (Char -> Bool) -> 106 | Parsec.ParsecT s u m (Character.Character tag) 107 | unencodedCharacter = fmap Character.Unencoded . Parsec.satisfy 108 | 109 | isFieldCharacter :: Char -> Bool 110 | isFieldCharacter x = case x of 111 | '_' -> True 112 | _ -> Char.isAsciiUpper x || Char.isAsciiLower x || Char.isDigit x 113 | 114 | modifier :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Modifier.Modifier 115 | modifier = 116 | Parsec.option Modifier.None $ 117 | Parsec.choice 118 | [ Modifier.Asterisk <$ Parsec.char '*', 119 | Parsec.char ':' >> Modifier.Colon <$> maxLength 120 | ] 121 | 122 | maxLength :: 123 | (Parsec.Stream s m Char) => Parsec.ParsecT s u m MaxLength.MaxLength 124 | maxLength = do 125 | x <- Parsec.satisfy $ Ix.inRange ('1', '9') 126 | xs <- Parsec.many $ Parsec.satisfy Char.isDigit 127 | n <- maybe (fail "invalid MaxLength") pure . Read.readMaybe $ x : xs 128 | if isMaxLength n 129 | then pure $ MaxLength.MaxLength n 130 | else fail "invalid MaxLength" 131 | 132 | isMaxLength :: Int -> Bool 133 | isMaxLength = Ix.inRange (1, 9999) 134 | 135 | literal :: (Parsec.Stream s m Char) => Parsec.ParsecT s u m Literal.Literal 136 | literal = Literal.Literal <$> nonEmpty literalCharacter 137 | 138 | literalCharacter :: 139 | (Parsec.Stream s m Char) => 140 | Parsec.ParsecT s u m (Character.Character Literal.Literal) 141 | literalCharacter = 142 | choice encodedCharacter (unencodedCharacter isLiteralCharacter) 143 | 144 | isLiteralCharacter :: Char -> Bool 145 | isLiteralCharacter x = case x of 146 | ' ' -> False 147 | '"' -> False 148 | '\'' -> False 149 | '%' -> False 150 | '<' -> False 151 | '>' -> False 152 | '\\' -> False 153 | '^' -> False 154 | '`' -> False 155 | '{' -> False 156 | '|' -> False 157 | '}' -> False 158 | _ -> 159 | Ix.inRange ('\x20', '\x7e') x 160 | || Ix.inRange ('\xa0', '\xd7ff') x 161 | || Ix.inRange ('\xe000', '\xf8ff') x 162 | || Ix.inRange ('\xf900', '\xfdcf') x 163 | || Ix.inRange ('\xfdf0', '\xffef') x 164 | || Ix.inRange ('\x10000', '\x1fffd') x 165 | || Ix.inRange ('\x20000', '\x2fffd') x 166 | || Ix.inRange ('\x30000', '\x3fffd') x 167 | || Ix.inRange ('\x40000', '\x4fffd') x 168 | || Ix.inRange ('\x50000', '\x5fffd') x 169 | || Ix.inRange ('\x60000', '\x6fffd') x 170 | || Ix.inRange ('\x70000', '\x7fffd') x 171 | || Ix.inRange ('\x80000', '\x8fffd') x 172 | || Ix.inRange ('\x90000', '\x9fffd') x 173 | || Ix.inRange ('\xa0000', '\xafffd') x 174 | || Ix.inRange ('\xb0000', '\xbfffd') x 175 | || Ix.inRange ('\xc0000', '\xcfffd') x 176 | || Ix.inRange ('\xd0000', '\xdfffd') x 177 | || Ix.inRange ('\xe1000', '\xefffd') x 178 | || Ix.inRange ('\xf0000', '\xffffd') x 179 | || Ix.inRange ('\x100000', '\x10fffd') x 180 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Match.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-missing-export-lists #-} 2 | 3 | module Burrito.Internal.Match where 4 | 5 | import qualified Burrito.Internal.Expand as Expand 6 | import qualified Burrito.Internal.Render as Render 7 | import qualified Burrito.Internal.Type.Case as Case 8 | import qualified Burrito.Internal.Type.Character as Character 9 | import qualified Burrito.Internal.Type.Digit as Digit 10 | import qualified Burrito.Internal.Type.Expression as Expression 11 | import qualified Burrito.Internal.Type.Literal as Literal 12 | import qualified Burrito.Internal.Type.Match as Match 13 | import qualified Burrito.Internal.Type.MaxLength as MaxLength 14 | import qualified Burrito.Internal.Type.Modifier as Modifier 15 | import qualified Burrito.Internal.Type.Name as Name 16 | import qualified Burrito.Internal.Type.Operator as Operator 17 | import qualified Burrito.Internal.Type.Template as Template 18 | import qualified Burrito.Internal.Type.Token as Token 19 | import qualified Burrito.Internal.Type.Value as Value 20 | import qualified Burrito.Internal.Type.Variable as Variable 21 | import qualified Control.Monad as Monad 22 | import qualified Data.ByteString as ByteString 23 | import qualified Data.Char as Char 24 | import qualified Data.List as List 25 | import qualified Data.List.NonEmpty as NonEmpty 26 | import qualified Data.Maybe as Maybe 27 | import qualified Data.Text as Text 28 | import qualified Data.Text.Encoding as Text 29 | import qualified Text.ParserCombinators.ReadP as ReadP 30 | 31 | -- | Matches a string against a template. This is essentially the opposite of 32 | -- @expand@. 33 | -- 34 | -- Since there isn't always one unique match, this function returns all the 35 | -- possibilities. It's up to you to select the one that makes the most sense, 36 | -- or to simply grab the first one if you don't care. 37 | -- 38 | -- >>> match "" <$> parse "no-match" 39 | -- Just [] 40 | -- >>> match "no-variables" <$> parse "no-variables" 41 | -- Just [[]] 42 | -- >>> match "1-match" <$> parse "{one}-match" 43 | -- Just [[("one",String "1")]] 44 | -- 45 | -- Be warned that the number of possible matches can grow quickly if your 46 | -- template has variables next to each other without any separators. 47 | -- 48 | -- >>> let Just template = parse "{a}{b}" 49 | -- >>> mapM_ print $ match "ab" template 50 | -- [("a",String "a"),("b",String "b")] 51 | -- [("a",String "ab"),("b",String "")] 52 | -- [("a",String "ab")] 53 | -- [("a",String ""),("b",String "ab")] 54 | -- [("b",String "ab")] 55 | -- 56 | -- Matching supports everything /except/ explode modifiers (@{a*}@), list 57 | -- values, and dictionary values. 58 | match :: String -> Template.Template -> [[(String, Value.Value)]] 59 | match s = 60 | fmap finalize 61 | . Maybe.mapMaybe (keepConsistent . fst) 62 | . flip ReadP.readP_to_S s 63 | . template 64 | 65 | finalize :: [(Name.Name, Match.Match)] -> [(String, Value.Value)] 66 | finalize = Maybe.mapMaybe $ \(n, m) -> case m of 67 | Match.Defined v -> 68 | Just (Render.builderToString $ Render.name n, Value.String v) 69 | Match.Prefix _ v -> 70 | Just (Render.builderToString $ Render.name n, Value.String v) 71 | Match.Undefined -> Nothing 72 | 73 | keepConsistent :: 74 | [(Name.Name, Match.Match)] -> Maybe [(Name.Name, Match.Match)] 75 | keepConsistent xs = case xs of 76 | [] -> Just xs 77 | (k, v) : ys -> do 78 | let (ts, fs) = List.partition ((== k) . fst) ys 79 | w <- combine v $ fmap snd ts 80 | ((k, w) :) <$> keepConsistent fs 81 | 82 | combine :: Match.Match -> [Match.Match] -> Maybe Match.Match 83 | combine x ys = case ys of 84 | [] -> Just x 85 | y : zs -> case x of 86 | Match.Defined t -> case y of 87 | Match.Defined u | t == u -> combine x zs 88 | Match.Prefix m u | Text.take (MaxLength.count m) t == u -> combine x zs 89 | _ -> Nothing 90 | Match.Prefix n t -> case y of 91 | Match.Defined u | t == Text.take (MaxLength.count n) u -> combine y zs 92 | Match.Prefix m u 93 | | let c = MaxLength.count (min n m) in Text.take c t == Text.take c u -> 94 | combine (if m > n then y else x) zs 95 | _ -> Nothing 96 | Match.Undefined -> case y of 97 | Match.Undefined -> combine x zs 98 | _ -> Nothing 99 | 100 | template :: Template.Template -> ReadP.ReadP [(Name.Name, Match.Match)] 101 | template x = do 102 | xs <- fmap mconcat . traverse token $ Template.tokens x 103 | ReadP.eof 104 | pure xs 105 | 106 | token :: Token.Token -> ReadP.ReadP [(Name.Name, Match.Match)] 107 | token x = case x of 108 | Token.Expression y -> expression y 109 | Token.Literal y -> [] <$ literal y 110 | 111 | expression :: Expression.Expression -> ReadP.ReadP [(Name.Name, Match.Match)] 112 | expression x = variables (Expression.operator x) (Expression.variables x) 113 | 114 | variables :: 115 | Operator.Operator -> 116 | NonEmpty.NonEmpty Variable.Variable -> 117 | ReadP.ReadP [(Name.Name, Match.Match)] 118 | variables op vs = case op of 119 | Operator.Ampersand -> vars vs (Just '&') '&' varEq 120 | Operator.FullStop -> vars vs (Just '.') '.' $ variable Expand.isUnreserved 121 | Operator.None -> vars vs Nothing ',' $ variable Expand.isUnreserved 122 | Operator.NumberSign -> vars vs (Just '#') ',' $ variable Expand.isAllowed 123 | Operator.PlusSign -> vars vs Nothing ',' $ variable Expand.isAllowed 124 | Operator.QuestionMark -> vars vs (Just '?') '&' varEq 125 | Operator.Semicolon -> vars vs (Just ';') ';' $ \v -> do 126 | let n = Variable.name v 127 | name n 128 | ReadP.option [(n, Match.Defined Text.empty)] $ do 129 | char_ '=' 130 | variable Expand.isUnreserved v 131 | Operator.Solidus -> vars vs (Just '/') '/' $ variable Expand.isUnreserved 132 | 133 | vars :: 134 | NonEmpty.NonEmpty Variable.Variable -> 135 | Maybe Char -> 136 | Char -> 137 | (Variable.Variable -> ReadP.ReadP [(Name.Name, Match.Match)]) -> 138 | ReadP.ReadP [(Name.Name, Match.Match)] 139 | vars vs m c f = do 140 | let ctx = case m of 141 | Nothing -> id 142 | Just o -> \p -> ReadP.option (undef <$> NonEmpty.toList vs) $ do 143 | char_ o 144 | xs <- p 145 | Monad.guard . not $ all isUndefined xs 146 | pure xs 147 | ctx . vars' c f $ NonEmpty.toList vs 148 | 149 | isUndefined :: (Name.Name, Match.Match) -> Bool 150 | isUndefined = (== Match.Undefined) . snd 151 | 152 | vars' :: 153 | Char -> 154 | (Variable.Variable -> ReadP.ReadP [(Name.Name, Match.Match)]) -> 155 | [Variable.Variable] -> 156 | ReadP.ReadP [(Name.Name, Match.Match)] 157 | vars' c f vs = case vs of 158 | [] -> pure [] 159 | v : ws -> 160 | let this = do 161 | x <- f v 162 | xs <- ReadP.option (undef <$> ws) $ do 163 | char_ c 164 | vars' c f ws 165 | pure $ x <> xs 166 | that = (undef v :) <$> vars' c f ws 167 | in this ReadP.+++ that 168 | 169 | undef :: Variable.Variable -> (Name.Name, Match.Match) 170 | undef v = (Variable.name v, Match.Undefined) 171 | 172 | char_ :: Char -> ReadP.ReadP () 173 | char_ = Monad.void . ReadP.char 174 | 175 | varEq :: Variable.Variable -> ReadP.ReadP [(Name.Name, Match.Match)] 176 | varEq v = do 177 | name $ Variable.name v 178 | char_ '=' 179 | variable Expand.isUnreserved v 180 | 181 | name :: Name.Name -> ReadP.ReadP () 182 | name = Monad.void . ReadP.string . Render.builderToString . Render.name 183 | 184 | variable :: 185 | (Char -> Bool) -> 186 | Variable.Variable -> 187 | ReadP.ReadP [(Name.Name, Match.Match)] 188 | variable f x = do 189 | v <- case Variable.modifier x of 190 | Modifier.Asterisk -> ReadP.pfail 191 | Modifier.None -> Match.Defined <$> manyCharacters f 192 | Modifier.Colon n -> Match.Prefix n <$> manyCharacters f 193 | pure [(Variable.name x, v)] 194 | 195 | manyCharacters :: (Char -> Bool) -> ReadP.ReadP Text.Text 196 | manyCharacters f = do 197 | let f1 = (:) <$> someEncodedCharacters <*> ReadP.option [] f2 198 | f2 = (:) <$> someUnencodedCharacters f <*> ReadP.option [] f1 199 | fmap mconcat . ReadP.option [] $ f1 ReadP.<++ f2 200 | 201 | someEncodedCharacters :: ReadP.ReadP Text.Text 202 | someEncodedCharacters = do 203 | xs <- some anEncodedCharacter 204 | either (fail . show) pure 205 | . Text.decodeUtf8' 206 | . ByteString.pack 207 | . fmap (uncurry Digit.toWord8) 208 | $ NonEmpty.toList xs 209 | 210 | some :: ReadP.ReadP a -> ReadP.ReadP (NonEmpty.NonEmpty a) 211 | some p = (NonEmpty.:|) <$> p <*> ReadP.many p 212 | 213 | someUnencodedCharacters :: (Char -> Bool) -> ReadP.ReadP Text.Text 214 | someUnencodedCharacters f = do 215 | xs <- some $ ReadP.satisfy f 216 | pure . Text.pack $ NonEmpty.toList xs 217 | 218 | anEncodedCharacter :: ReadP.ReadP (Digit.Digit, Digit.Digit) 219 | anEncodedCharacter = do 220 | char_ '%' 221 | (,) <$> aDigit <*> aDigit 222 | 223 | aDigit :: ReadP.ReadP Digit.Digit 224 | aDigit = do 225 | x <- ReadP.satisfy Char.isHexDigit 226 | maybe (fail "invalid Digit") pure $ Digit.fromChar x 227 | 228 | literal :: Literal.Literal -> ReadP.ReadP () 229 | literal = mapM_ literalCharacter . Literal.characters 230 | 231 | literalCharacter :: Character.Character Literal.Literal -> ReadP.ReadP () 232 | literalCharacter = character Expand.isAllowed 233 | 234 | character :: (Char -> Bool) -> Character.Character tag -> ReadP.ReadP () 235 | character f x = case x of 236 | Character.Encoded y z -> encodedCharacter y z 237 | Character.Unencoded y -> unencodedCharacter f y 238 | 239 | encodedCharacter :: Digit.Digit -> Digit.Digit -> ReadP.ReadP () 240 | encodedCharacter x y = char_ '%' *> digit x *> digit y 241 | 242 | digit :: Digit.Digit -> ReadP.ReadP () 243 | digit x = char_ $ case x of 244 | Digit.Ox0 -> '0' 245 | Digit.Ox1 -> '1' 246 | Digit.Ox2 -> '2' 247 | Digit.Ox3 -> '3' 248 | Digit.Ox4 -> '4' 249 | Digit.Ox5 -> '5' 250 | Digit.Ox6 -> '6' 251 | Digit.Ox7 -> '7' 252 | Digit.Ox8 -> '8' 253 | Digit.Ox9 -> '9' 254 | Digit.OxA Case.Upper -> 'A' 255 | Digit.OxB Case.Upper -> 'B' 256 | Digit.OxC Case.Upper -> 'C' 257 | Digit.OxD Case.Upper -> 'D' 258 | Digit.OxE Case.Upper -> 'E' 259 | Digit.OxF Case.Upper -> 'F' 260 | Digit.OxA Case.Lower -> 'a' 261 | Digit.OxB Case.Lower -> 'b' 262 | Digit.OxC Case.Lower -> 'c' 263 | Digit.OxD Case.Lower -> 'd' 264 | Digit.OxE Case.Lower -> 'e' 265 | Digit.OxF Case.Lower -> 'f' 266 | 267 | unencodedCharacter :: (Char -> Bool) -> Char -> ReadP.ReadP () 268 | unencodedCharacter f x = 269 | if f x 270 | then char_ x 271 | else mapM_ (uncurry encodedCharacter) $ Expand.encodeCharacter x 272 | -------------------------------------------------------------------------------- /source/library/Burrito/Internal/Expand.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -Wno-missing-export-lists #-} 2 | 3 | module Burrito.Internal.Expand where 4 | 5 | import qualified Burrito.Internal.Render as Render 6 | import qualified Burrito.Internal.Type.Character as Character 7 | import qualified Burrito.Internal.Type.Digit as Digit 8 | import qualified Burrito.Internal.Type.Expression as Expression 9 | import qualified Burrito.Internal.Type.Field as Field 10 | import qualified Burrito.Internal.Type.Literal as Literal 11 | import qualified Burrito.Internal.Type.MaxLength as MaxLength 12 | import qualified Burrito.Internal.Type.Modifier as Modifier 13 | import qualified Burrito.Internal.Type.Name as Name 14 | import qualified Burrito.Internal.Type.Operator as Operator 15 | import qualified Burrito.Internal.Type.Template as Template 16 | import qualified Burrito.Internal.Type.Token as Token 17 | import qualified Burrito.Internal.Type.Value as Value 18 | import qualified Burrito.Internal.Type.Variable as Variable 19 | import qualified Control.Monad.Trans.Class as Trans 20 | import qualified Control.Monad.Trans.State as State 21 | import qualified Data.ByteString as ByteString 22 | import qualified Data.Char as Char 23 | import qualified Data.Functor.Identity as Identity 24 | import qualified Data.List as List 25 | import qualified Data.List.NonEmpty as NonEmpty 26 | import qualified Data.Map as Map 27 | import qualified Data.Maybe as Maybe 28 | import qualified Data.Text as Text 29 | import qualified Data.Text.Encoding as Text 30 | import qualified Data.Text.Lazy as LazyText 31 | import qualified Data.Text.Lazy.Builder as Builder 32 | 33 | -- | Expands a template using the given values. Unlike parsing, expansion 34 | -- always succeeds. If no value is given for a variable, it will simply not 35 | -- appear in the output. 36 | -- 37 | -- >>> expand [] <$> parse "valid-template" 38 | -- Just "valid-template" 39 | -- >>> expand [] <$> parse "template:{example}" 40 | -- Just "template:" 41 | -- >>> expand [("example", stringValue "true")] <$> parse "template:{example}" 42 | -- Just "template:true" 43 | expand :: [(String, Value.Value)] -> Template.Template -> String 44 | expand values = 45 | let m = Map.mapKeys Text.pack $ Map.fromList values 46 | in Render.builderToString 47 | . Identity.runIdentity 48 | . expandWith 49 | (pure . flip Map.lookup m) 50 | 51 | -- | This is like @expand@ except that it gives you more control over how 52 | -- variables are expanded. If you can, use @expand@. It's simpler. 53 | -- 54 | -- Instead of passing in a static mapping from names to 55 | -- values, you pass in a function that is used to look up values on the fly. 56 | -- This can be useful if computing values takes a while or requires some impure 57 | -- actions. 58 | -- 59 | -- >>> expandWith (\ x -> [Nothing, Just . stringValue $ unpack x]) <$> parse "template:{example}" 60 | -- Just ["template:","template:example"] 61 | -- >>> let Just template = parse "user={USER}" 62 | -- >>> expandWith (fmap (fmap stringValue) . lookupEnv . unpack) template 63 | -- "user=taylor" 64 | -- 65 | -- Note that as the RFC specifies, the given function will be called at most 66 | -- once for each variable in the template. 67 | -- 68 | -- >>> let Just template = parse "{a}{a}" 69 | -- >>> expandWith (\ x -> do { putStrLn $ "-- expanding " <> show x; pure . Just $ Burrito.stringValue "A" }) template 70 | -- -- expanding "a" 71 | -- "AA" 72 | expandWith :: 73 | (Monad m) => 74 | (Text.Text -> m (Maybe Value.Value)) -> 75 | Template.Template -> 76 | m Builder.Builder 77 | expandWith f = flip State.evalStateT Map.empty . template (cached f) 78 | 79 | type CacheT = State.StateT (Map.Map Text.Text (Maybe Value.Value)) 80 | 81 | cached :: 82 | (Monad m) => 83 | (Text.Text -> m (Maybe Value.Value)) -> 84 | Name.Name -> 85 | CacheT m (Maybe Value.Value) 86 | cached f x = do 87 | let key = LazyText.toStrict . Builder.toLazyText $ name x 88 | cache <- State.get 89 | case Map.lookup key cache of 90 | Just result -> pure result 91 | Nothing -> do 92 | result <- Trans.lift $ f key 93 | State.modify $ Map.insert key result 94 | pure result 95 | 96 | template :: 97 | (Monad m) => 98 | (Name.Name -> CacheT m (Maybe Value.Value)) -> 99 | Template.Template -> 100 | CacheT m Builder.Builder 101 | template f = fmap mconcat . traverse (token f) . Template.tokens 102 | 103 | token :: 104 | (Monad m) => 105 | (Name.Name -> CacheT m (Maybe Value.Value)) -> 106 | Token.Token -> 107 | CacheT m Builder.Builder 108 | token f x = case x of 109 | Token.Expression y -> expression f y 110 | Token.Literal y -> pure $ literal y 111 | 112 | expression :: 113 | (Monad m) => 114 | (Name.Name -> CacheT m (Maybe Value.Value)) -> 115 | Expression.Expression -> 116 | CacheT m Builder.Builder 117 | expression f ex = 118 | let op = Expression.operator ex 119 | in fmap 120 | ( mconcat 121 | . (\xs -> if null xs then xs else prefix op : xs) 122 | . List.intersperse (separator op) 123 | . Maybe.catMaybes 124 | ) 125 | . traverse (variable f op) 126 | . NonEmpty.toList 127 | $ Expression.variables ex 128 | 129 | separator :: Operator.Operator -> Builder.Builder 130 | separator op = Builder.singleton $ case op of 131 | Operator.Ampersand -> '&' 132 | Operator.FullStop -> '.' 133 | Operator.None -> ',' 134 | Operator.NumberSign -> ',' 135 | Operator.PlusSign -> ',' 136 | Operator.QuestionMark -> '&' 137 | Operator.Semicolon -> ';' 138 | Operator.Solidus -> '/' 139 | 140 | prefix :: Operator.Operator -> Builder.Builder 141 | prefix op = case op of 142 | Operator.Ampersand -> Builder.singleton '&' 143 | Operator.FullStop -> Builder.singleton '.' 144 | Operator.None -> mempty 145 | Operator.NumberSign -> Builder.singleton '#' 146 | Operator.PlusSign -> mempty 147 | Operator.QuestionMark -> Builder.singleton '?' 148 | Operator.Semicolon -> Builder.singleton ';' 149 | Operator.Solidus -> Builder.singleton '/' 150 | 151 | variable :: 152 | (Monad m) => 153 | (Name.Name -> CacheT m (Maybe Value.Value)) -> 154 | Operator.Operator -> 155 | Variable.Variable -> 156 | CacheT m (Maybe Builder.Builder) 157 | variable f op var = do 158 | res <- f $ Variable.name var 159 | pure $ case res of 160 | Nothing -> Nothing 161 | Just val -> value op var val 162 | 163 | value :: 164 | Operator.Operator -> 165 | Variable.Variable -> 166 | Value.Value -> 167 | Maybe Builder.Builder 168 | value op var val = case val of 169 | Value.Dictionary xs -> dictionaryValue op var $ Map.toAscList xs 170 | Value.List xs -> listValue op var xs 171 | Value.String x -> Just $ stringValue op var x 172 | 173 | dictionaryValue :: 174 | Operator.Operator -> 175 | Variable.Variable -> 176 | [(Text.Text, Text.Text)] -> 177 | Maybe Builder.Builder 178 | dictionaryValue = items $ \op var (k, v) -> 179 | let f = string op Modifier.None 180 | in case Variable.modifier var of 181 | Modifier.Asterisk -> [f k <> Builder.singleton '=' <> f v] 182 | _ -> [f k, f v] 183 | 184 | listValue :: 185 | Operator.Operator -> 186 | Variable.Variable -> 187 | [Text.Text] -> 188 | Maybe Builder.Builder 189 | listValue = items $ \op var -> 190 | pure 191 | . stringValue 192 | ( case Variable.modifier var of 193 | Modifier.Asterisk -> op 194 | _ -> Operator.None 195 | ) 196 | var {Variable.modifier = Modifier.None} 197 | 198 | items :: 199 | (Operator.Operator -> Variable.Variable -> a -> [Builder.Builder]) -> 200 | Operator.Operator -> 201 | Variable.Variable -> 202 | [a] -> 203 | Maybe Builder.Builder 204 | items f op var xs = 205 | let md = Variable.modifier var 206 | sep = case md of 207 | Modifier.Asterisk -> separator op 208 | _ -> Builder.singleton ',' 209 | p = case md of 210 | Modifier.Asterisk -> False 211 | _ -> case op of 212 | Operator.Ampersand -> True 213 | Operator.QuestionMark -> True 214 | Operator.Semicolon -> True 215 | _ -> False 216 | in if null xs 217 | then Nothing 218 | else 219 | Just 220 | . mconcat 221 | . (if p then (label True var :) else id) 222 | . List.intersperse sep 223 | $ concatMap (f op var) xs 224 | 225 | label :: Bool -> Variable.Variable -> Builder.Builder 226 | label p v = 227 | name (Variable.name v) <> if p then Builder.singleton '=' else mempty 228 | 229 | name :: Name.Name -> Builder.Builder 230 | name = 231 | mconcat 232 | . List.intersperse (Builder.singleton '.') 233 | . fmap field 234 | . NonEmpty.toList 235 | . Name.fields 236 | 237 | field :: Field.Field -> Builder.Builder 238 | field = foldMap (character $ const True) . Field.characters 239 | 240 | character :: (Char -> Bool) -> Character.Character tag -> Builder.Builder 241 | character f x = case x of 242 | Character.Encoded y z -> Render.encodedCharacter y z 243 | Character.Unencoded y -> unencodedCharacter f y 244 | 245 | stringValue :: 246 | Operator.Operator -> Variable.Variable -> Text.Text -> Builder.Builder 247 | stringValue op var str = 248 | let pre = case op of 249 | Operator.Ampersand -> label True var 250 | Operator.QuestionMark -> label True var 251 | Operator.Semicolon -> label (not $ Text.null str) var 252 | _ -> mempty 253 | in pre <> string op (Variable.modifier var) str 254 | 255 | string :: 256 | Operator.Operator -> Modifier.Modifier -> Text.Text -> Builder.Builder 257 | string op md = 258 | let allowed x = case op of 259 | Operator.NumberSign -> isAllowed x 260 | Operator.PlusSign -> isAllowed x 261 | _ -> isUnreserved x 262 | trim = case md of 263 | Modifier.Colon ml -> Text.take $ MaxLength.count ml 264 | _ -> id 265 | in foldMap (unencodedCharacter allowed) . Text.unpack . trim 266 | 267 | isAllowed :: Char -> Bool 268 | isAllowed x = isUnreserved x || isReserved x 269 | 270 | isUnreserved :: Char -> Bool 271 | isUnreserved x = case x of 272 | '-' -> True 273 | '.' -> True 274 | '_' -> True 275 | '~' -> True 276 | _ -> Char.isAsciiUpper x || Char.isAsciiLower x || Char.isDigit x 277 | 278 | isReserved :: Char -> Bool 279 | isReserved x = case x of 280 | '!' -> True 281 | '$' -> True 282 | '&' -> True 283 | '\'' -> True 284 | '(' -> True 285 | ')' -> True 286 | '*' -> True 287 | '+' -> True 288 | ',' -> True 289 | ';' -> True 290 | '=' -> True 291 | ':' -> True 292 | '/' -> True 293 | '?' -> True 294 | '#' -> True 295 | '[' -> True 296 | ']' -> True 297 | '@' -> True 298 | _ -> False 299 | 300 | unencodedCharacter :: (Char -> Bool) -> Char -> Builder.Builder 301 | unencodedCharacter f x = 302 | if f x 303 | then Builder.singleton x 304 | else foldMap (uncurry Render.encodedCharacter) $ encodeCharacter x 305 | 306 | encodeCharacter :: Char -> [(Digit.Digit, Digit.Digit)] 307 | encodeCharacter = 308 | fmap Digit.fromWord8 . ByteString.unpack . Text.encodeUtf8 . Text.singleton 309 | 310 | literal :: Literal.Literal -> Builder.Builder 311 | literal = foldMap (character isAllowed) . Literal.characters 312 | -------------------------------------------------------------------------------- /source/test-suite/Main.hs: -------------------------------------------------------------------------------- 1 | {- hlint ignore "Redundant bracket" -} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE QuasiQuotes #-} 4 | {-# OPTIONS_GHC -Wno-monomorphism-restriction #-} 5 | 6 | module Main 7 | ( main, 8 | ) 9 | where 10 | 11 | import qualified Burrito 12 | import qualified Burrito.Internal.Parse as Parse 13 | import qualified Burrito.Internal.Render as Render 14 | import qualified Burrito.Internal.Type.Case as Case 15 | import qualified Burrito.Internal.Type.Character as Character 16 | import qualified Burrito.Internal.Type.Digit as Digit 17 | import qualified Burrito.Internal.Type.Expression as Expression 18 | import qualified Burrito.Internal.Type.Field as Field 19 | import qualified Burrito.Internal.Type.Literal as Literal 20 | import qualified Burrito.Internal.Type.MaxLength as MaxLength 21 | import qualified Burrito.Internal.Type.Modifier as Modifier 22 | import qualified Burrito.Internal.Type.Name as Name 23 | import qualified Burrito.Internal.Type.Operator as Operator 24 | import qualified Burrito.Internal.Type.Template as Template 25 | import qualified Burrito.Internal.Type.Token as Token 26 | import qualified Burrito.Internal.Type.Value as Value 27 | import qualified Burrito.Internal.Type.Variable as Variable 28 | import qualified Control.Monad as Monad 29 | import qualified Data.List as List 30 | import qualified Data.List.NonEmpty as NonEmpty 31 | import qualified Data.Map as Map 32 | import qualified Data.Maybe as Maybe 33 | import qualified Data.Set as Set 34 | import qualified Data.String as String 35 | import qualified Data.Text as Text 36 | import qualified GHC.Stack as Stack 37 | import qualified Test.Hspec as Hspec 38 | import qualified Test.Hspec.QuickCheck as Hspec 39 | import qualified Test.QuickCheck as QC 40 | 41 | main :: IO () 42 | main = Hspec.hspec . Hspec.describe "Burrito" $ do 43 | Monad.forM_ tests $ \test -> 44 | Hspec.it (show (testInput test, unwrapOutput $ testOutput test)) $ 45 | runTest test 46 | 47 | Hspec.describe "match" $ do 48 | let matchTest :: 49 | (Stack.HasCallStack) => 50 | String -> 51 | [(String, Burrito.Value)] -> 52 | String -> 53 | Hspec.Spec 54 | matchTest input values output = Hspec.it (show (input, output)) $ do 55 | template <- maybe (fail "invalid Template") pure $ Burrito.parse input 56 | let matches = Burrito.match output template 57 | matches `Hspec.shouldSatisfy` elem values 58 | Monad.forM_ matches $ \match -> do 59 | let it = Burrito.expand match template 60 | Monad.when (it /= output) . Hspec.expectationFailure $ 61 | show 62 | (input, values, output, matches, match, it) 63 | 64 | matchTest "" [] "" 65 | matchTest "a" [] "a" 66 | matchTest "!" [] "!" 67 | matchTest "\xa0" [] "%C2%A0" 68 | 69 | matchTest "{a}" ["a" =: s ""] "" 70 | matchTest "{a}" ["a" =: s "A"] "A" 71 | matchTest "{a}" ["a" =: s "AB"] "AB" 72 | 73 | matchTest "a{b}" ["b" =: s ""] "a" 74 | matchTest "a{b}" ["b" =: s "B"] "aB" 75 | matchTest "a{b}" ["b" =: s "BC"] "aBC" 76 | 77 | matchTest "{a}b" ["a" =: s ""] "b" 78 | matchTest "{a}b" ["a" =: s "A"] "Ab" 79 | matchTest "{a}b" ["a" =: s "AB"] "ABb" 80 | 81 | matchTest "{a}{a}" ["a" =: s "A"] "AA" 82 | matchTest "{a}{a}" ["a" =: s "AB"] "ABAB" 83 | 84 | matchTest "{a}" ["a" =: s "%"] "%25" 85 | matchTest "{a}" ["a" =: s "/"] "%2F" 86 | matchTest "{a}" ["a" =: s "\xa0"] "%C2%A0" 87 | matchTest "{a}" ["a" =: s "\xd7ff"] "%ED%9F%BF" 88 | matchTest "{a}" ["a" =: s "\x10000"] "%F0%90%80%80" 89 | matchTest "{a}" ["a" =: s "A/B"] "A%2FB" 90 | matchTest "{a}" ["a" =: s "WX\xa0\xa1YZ"] "WX%C2%A0%C2%A1YZ" 91 | 92 | matchTest "{+a}" ["a" =: s "%"] "%25" 93 | matchTest "{+a}" ["a" =: s "/"] "/" 94 | 95 | matchTest "{#a}" [] "" 96 | matchTest "{#a}" ["a" =: s ""] "#" 97 | matchTest "{#a}" ["a" =: s "A"] "#A" 98 | matchTest "{#a}" ["a" =: s "%"] "#%25" 99 | matchTest "{#a}" ["a" =: s "/"] "#/" 100 | 101 | matchTest "{.a}" [] "" 102 | matchTest "{.a}" ["a" =: s ""] "." 103 | matchTest "{.a}" ["a" =: s "A"] ".A" 104 | matchTest "{.a}" ["a" =: s "%"] ".%25" 105 | matchTest "{.a}" ["a" =: s "/"] ".%2F" 106 | 107 | matchTest "{/a}" [] "" 108 | matchTest "{/a}" ["a" =: s ""] "/" 109 | matchTest "{/a}" ["a" =: s "A"] "/A" 110 | matchTest "{/a}" ["a" =: s "%"] "/%25" 111 | matchTest "{/a}" ["a" =: s "/"] "/%2F" 112 | 113 | matchTest "{;a}" [] "" 114 | matchTest "{;a}" ["a" =: s ""] ";a" 115 | matchTest "{;a}" ["a" =: s "A"] ";a=A" 116 | matchTest "{;a}" ["a" =: s "%"] ";a=%25" 117 | matchTest "{;a}" ["a" =: s "/"] ";a=%2F" 118 | 119 | matchTest "{?a}" [] "" 120 | matchTest "{?a}" ["a" =: s ""] "?a=" 121 | matchTest "{?a}" ["a" =: s "A"] "?a=A" 122 | matchTest "{?a}" ["a" =: s "%"] "?a=%25" 123 | matchTest "{?a}" ["a" =: s "/"] "?a=%2F" 124 | 125 | matchTest "{&a}" [] "" 126 | matchTest "{&a}" ["a" =: s ""] "&a=" 127 | matchTest "{&a}" ["a" =: s "A"] "&a=A" 128 | matchTest "{&a}" ["a" =: s "%"] "&a=%25" 129 | matchTest "{&a}" ["a" =: s "/"] "&a=%2F" 130 | 131 | matchTest "{a,b}" ["a" =: s "A", "b" =: s "B"] "A,B" 132 | matchTest "{+a,b}" ["a" =: s "A", "b" =: s "B"] "A,B" 133 | matchTest "{#a,b}" ["a" =: s "A", "b" =: s "B"] "#A,B" 134 | matchTest "{.a,b}" ["a" =: s "A", "b" =: s "B"] ".A.B" 135 | matchTest "{/a,b}" ["a" =: s "A", "b" =: s "B"] "/A/B" 136 | matchTest "{;a,b}" ["a" =: s "A", "b" =: s "B"] ";a=A;b=B" 137 | matchTest "{?a,b}" ["a" =: s "A", "b" =: s "B"] "?a=A&b=B" 138 | matchTest "{&a,b}" ["a" =: s "A", "b" =: s "B"] "&a=A&b=B" 139 | 140 | matchTest "{a,b}" ["a" =: s "A"] "A" 141 | matchTest "{a,b,c}" ["a" =: s "A"] "A" 142 | matchTest "{a,b,c}" ["a" =: s "A", "b" =: s "B"] "A,B" 143 | matchTest "{a}{a,b}" ["b" =: s "B"] "B" 144 | matchTest "{a}{a,b,c}" ["b" =: s "B"] "B" 145 | matchTest "{a,b}{a,b,c}" ["c" =: s "C"] "C" 146 | matchTest "{b}{a,b,c}" ["a" =: s "A", "c" =: s "C"] "A,C" 147 | matchTest "{a}{a,b,c}" ["b" =: s "B", "c" =: s "C"] "B,C" 148 | 149 | matchTest "{a:1}/{a}" ["a" =: s "AB"] "A/AB" 150 | 151 | -- TODO: Test matching on explode modifier. 152 | -- matchTest "{a*}" ["a" =: s "A"] "A" 153 | 154 | -- TODO: Test matching on lists. 155 | -- matchTest "{a}" ["a" =: l ["A", "B"]] "A,B" 156 | 157 | -- TODO: Test matching on dictionaries. 158 | -- matchTest "{a}" ["a" =: d ["k" =: "v"]] "k,v" 159 | 160 | Hspec.describe "uriTemplate" 161 | . Hspec.it "works as an expression" 162 | $ Just [Burrito.uriTemplate|a{b}c|] 163 | `Hspec.shouldBe` Burrito.parse "a{b}c" 164 | 165 | Hspec.modifyMaxSize (const 10) 166 | . Hspec.it "round trips" 167 | . QC.property 168 | $ \(Template template) -> Burrito.parse (show template) == Just template 169 | 170 | -- brittany-next-binding --columns 160 171 | tests :: [Test] 172 | tests = 173 | mconcat 174 | [ [ Test "" [] "", 175 | Test "" ["a" =: s ""] "", 176 | Test "" ["a" =: l []] "", 177 | Test "" ["a" =: d []] "", 178 | Test "!" [] "!", 179 | Test "#" [] "#", 180 | Test "$" [] "$", 181 | Test "&" [] "&", 182 | Test "(" [] "(", 183 | Test ")" [] ")", 184 | Test "*" [] "*", 185 | Test "+" [] "+", 186 | Test "," [] ",", 187 | Test "-" [] "-", 188 | Test "." [] ".", 189 | Test "/" [] "/", 190 | Test "0" [] "0", 191 | Test "9" [] "9", 192 | Test ":" [] ":", 193 | Test ";" [] ";", 194 | Test "=" [] "=", 195 | Test "?" [] "?", 196 | Test "@" [] "@", 197 | Test "A" [] "A", 198 | Test "Z" [] "Z", 199 | Test "[" [] "[", 200 | Test "]" [] "]", 201 | Test "_" [] "_", 202 | Test "a" [] "a", 203 | Test "z" [] "z", 204 | Test "~" [] "~", 205 | Test "\xa0" [] "%C2%A0", 206 | Test "\xd7ff" [] "%ED%9F%BF", 207 | Test "\xf900" [] "%EF%A4%80", 208 | Test "\xfdcf" [] "%EF%B7%8F", 209 | Test "\xfdf0" [] "%EF%B7%B0", 210 | Test "\xffef" [] "%EF%BF%AF", 211 | Test "\x10000" [] "%F0%90%80%80", 212 | Test "\x1fffd" [] "%F0%9F%BF%BD", 213 | Test "\x20000" [] "%F0%A0%80%80", 214 | Test "\x2fffd" [] "%F0%AF%BF%BD", 215 | Test "\x30000" [] "%F0%B0%80%80", 216 | Test "\x3fffd" [] "%F0%BF%BF%BD", 217 | Test "\x40000" [] "%F1%80%80%80", 218 | Test "\x4fffd" [] "%F1%8F%BF%BD", 219 | Test "\x50000" [] "%F1%90%80%80", 220 | Test "\x5fffd" [] "%F1%9F%BF%BD", 221 | Test "\x60000" [] "%F1%A0%80%80", 222 | Test "\x6fffd" [] "%F1%AF%BF%BD", 223 | Test "\x70000" [] "%F1%B0%80%80", 224 | Test "\x7fffd" [] "%F1%BF%BF%BD", 225 | Test "\x80000" [] "%F2%80%80%80", 226 | Test "\x8fffd" [] "%F2%8F%BF%BD", 227 | Test "\x90000" [] "%F2%90%80%80", 228 | Test "\x9fffd" [] "%F2%9F%BF%BD", 229 | Test "\xa0000" [] "%F2%A0%80%80", 230 | Test "\xafffd" [] "%F2%AF%BF%BD", 231 | Test "\xb0000" [] "%F2%B0%80%80", 232 | Test "\xbfffd" [] "%F2%BF%BF%BD", 233 | Test "\xc0000" [] "%F3%80%80%80", 234 | Test "\xcfffd" [] "%F3%8F%BF%BD", 235 | Test "\xd0000" [] "%F3%90%80%80", 236 | Test "\xdfffd" [] "%F3%9F%BF%BD", 237 | Test "\xe1000" [] "%F3%A1%80%80", 238 | Test "\xefffd" [] "%F3%AF%BF%BD", 239 | Test "\xe000" [] "%EE%80%80", 240 | Test "\xf8ff" [] "%EF%A3%BF", 241 | Test "\xf0000" [] "%F3%B0%80%80", 242 | Test "\xffffd" [] "%F3%BF%BF%BD", 243 | Test "\x100000" [] "%F4%80%80%80", 244 | Test "\x10fffd" [] "%F4%8F%BF%BD", 245 | Test "%00" [] "%00", 246 | Test "%AA" [] "%AA", 247 | Test "%Aa" [] "%Aa", 248 | Test "%aA" [] "%aA", 249 | Test "%aa" [] "%aa", 250 | Test "%" [] noParse, 251 | Test "%0" [] noParse, 252 | Test "%0z" [] noParse, 253 | Test "%z" [] noParse, 254 | Test "%30" [] "%30", 255 | Test " " [] noParse, 256 | Test "\"" [] noParse, 257 | Test "'" [] noParse, 258 | Test "%" [] noParse, 259 | Test "<" [] noParse, 260 | Test ">" [] noParse, 261 | Test "\\" [] noParse, 262 | Test "^" [] noParse, 263 | Test "`" [] noParse, 264 | Test "{" [] noParse, 265 | Test "|" [] noParse, 266 | Test "}" [] noParse, 267 | Test "{}" [] noParse, 268 | Test "{,}" [] noParse, 269 | Test "{a,,b}" [] noParse, 270 | Test "{+}" [] noParse, 271 | Test "{:1}" [] noParse, 272 | Test "{*}" [] noParse, 273 | Test "{AZ}" [] "", 274 | Test "{az}" [] "", 275 | Test "{09}" [] "", 276 | Test "{_a}" [] "", 277 | Test "{a_}" [] "", 278 | Test "{_}" [] "", 279 | Test "{A.A}" [] "", 280 | Test "{a.a}" [] "", 281 | Test "{0.0}" [] "", 282 | Test "{_._}" [] "", 283 | Test "{%aa.%aa}" [] "", 284 | Test "{.}" [] noParse, 285 | Test "{a.}" [] noParse, 286 | Test "{+.a}" [] noParse, 287 | Test "{a..b}" [] noParse, 288 | Test "{%00}" [] "", 289 | Test "{%}" [] noParse, 290 | Test "{%0}" [] noParse, 291 | Test "{%0z}" [] noParse, 292 | Test "{%z}" [] noParse, 293 | Test "{!}" [] noParse, 294 | Test "{" [] noParse, 295 | Test "{{}" [] noParse, 296 | Test "}" [] noParse, 297 | Test "{}}" [] noParse, 298 | Test "{a,b}" [] "", 299 | Test "{a,b,c,d}" [] "", 300 | Test "{a,a}" [] "", 301 | Test "{a:5}" [] "", 302 | Test "{a:67}" [] "", 303 | Test "{a:801}" [] "", 304 | Test "{a:234}" [] "", 305 | Test "{a:9999}" [] "", 306 | Test "{a:123}" ["a" =: s (replicate 200 'a')] . Output . Just $ replicate 123 'a', 307 | Test "{a:}" [] noParse, 308 | Test "{a:0}" [] noParse, 309 | Test "{a:10000}" [] noParse, 310 | Test "{a:-1}" [] noParse, 311 | Test "{a*}" [] "", 312 | Test "{a:1*}" [] noParse, 313 | Test "{a*:1}" [] noParse, 314 | Test "{a,b:1,c*}" [] "", 315 | Test "{+a}" [] "", 316 | Test "{#a}" [] "", 317 | Test "{.a}" [] "", 318 | Test "{/a}" [] "", 319 | Test "{;a}" [] "", 320 | Test "{?a}" [] "", 321 | Test "{&a}" [] "", 322 | Test "{=a}" [] noParse, 323 | Test "{,a}" [] noParse, 324 | Test "{!a}" [] noParse, 325 | Test "{@a}" [] noParse, 326 | Test "{|a}" [] noParse, 327 | Test "{+#a}" [] noParse, 328 | Test "{+a,#b}" [] noParse, 329 | Test "{+a:1}" [] "", 330 | Test "{#a*}" [] "", 331 | Test "{+a,b}" [] "", 332 | Test "{#a,b}" [] "", 333 | Test "{.a,b}" [] "", 334 | Test "{/a,b}" [] "", 335 | Test "{;a,b}" [] "", 336 | Test "{?a,b}" [] "", 337 | Test "{&a,b}" [] "", 338 | Test "{a}{b}" [] "", 339 | Test "{a}{b}{c}{d}" [] "", 340 | Test "{a}{a}" [] "", 341 | Test "{{}}" [] noParse, 342 | Test "{a{b}}" [] noParse, 343 | Test "{{a}b}" [] noParse, 344 | Test "{a{b}c}" [] noParse, 345 | Test "a{b}" [] "a", 346 | Test "{a}b" [] "b", 347 | Test "a{b}c" [] "ac", 348 | Test "{a}b{c}" [] "b", 349 | Test "{a}" [] "", 350 | Test "{+a}" [] "", 351 | Test "{#a}" [] "", 352 | Test "{.a}" [] "", 353 | Test "{/a}" [] "", 354 | Test "{;a}" [] "", 355 | Test "{?a}" [] "", 356 | Test "{&a}" [] "", 357 | Test "http://example.com/~{username}/" ["username" =: s "fred"] "http://example.com/~fred/", 358 | Test "http://example.com/~{username}/" ["username" =: s "mark"] "http://example.com/~mark/", 359 | Test "http://example.com/dictionary/{term:1}/{term}" ["term" =: s "cat"] "http://example.com/dictionary/c/cat", 360 | Test "http://example.com/dictionary/{term:1}/{term}" ["term" =: s "dog"] "http://example.com/dictionary/d/dog", 361 | Test "http://example.com/search{?q,lang}" ["q" =: s "cat", "lang" =: s "en"] "http://example.com/search?q=cat&lang=en", 362 | Test "http://example.com/search{?q,lang}" ["q" =: s "chien", "lang" =: s "fr"] "http://example.com/search?q=chien&lang=fr", 363 | Test "http://www.example.com/foo{?query,number}" ["query" =: s "mycelium", "number" =: s "100"] "http://www.example.com/foo?query=mycelium&number=100", 364 | Test "http://www.example.com/foo{?query,number}" ["number" =: s "100"] "http://www.example.com/foo?number=100", 365 | Test "http://www.example.com/foo{?query,number}" [] "http://www.example.com/foo", 366 | Test "{a}" [] "", 367 | Test "{a}" ["a" =: l []] "", 368 | Test "{a}" ["a" =: d []] "", 369 | Test "{a}" ["a" =: s ""] "", 370 | Test "{a}" ["a" =: s "A"] "A", 371 | Test "{a}" ["a" =: s "~"] "~", 372 | Test "{a}" ["a" =: s "%"] "%25", 373 | Test "{a}" ["a" =: s "?"] "%3F", 374 | Test "{a}" ["a" =: s "&"] "%26", 375 | Test "{a}" ["a" =: s "\xa0"] "%C2%A0", 376 | Test "{a}" ["a" =: s "\xd7ff"] "%ED%9F%BF", 377 | Test "{a}" ["a" =: s "\x10000"] "%F0%90%80%80", 378 | Test "{a}" ["a" =: l ["A"]] "A", 379 | Test "{a}" ["a" =: l ["A", "B"]] "A,B", 380 | Test "{a}" ["a" =: l ["%"]] "%25", 381 | Test "{a}" ["a" =: l ["\xa0"]] "%C2%A0", 382 | Test "{a}" ["a" =: d ["A" =: "1"]] "A,1", 383 | Test "{a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "A,1,B,2", 384 | Test "{a}" ["a" =: d ["A" =: "%"]] "A,%25", 385 | Test "{a}" ["a" =: d ["A" =: "\xa0"]] "A,%C2%A0", 386 | Test "{a}" ["a" =: d ["%" =: "1"]] "%25,1", 387 | Test "{a*}" [] "", 388 | Test "{a*}" ["a" =: s ""] "", 389 | Test "{a*}" ["a" =: s "A"] "A", 390 | Test "{a*}" ["a" =: l []] "", 391 | Test "{a*}" ["a" =: l ["A"]] "A", 392 | Test "{a*}" ["a" =: l ["A", "B"]] "A,B", 393 | Test "{a*}" ["a" =: d []] "", 394 | Test "{a*}" ["a" =: d ["A" =: "1"]] "A=1", 395 | Test "{a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "A=1,B=2", 396 | Test "{a:1}" [] "", 397 | Test "{a:1}" ["a" =: s ""] "", 398 | Test "{a:1}" ["a" =: s "A"] "A", 399 | Test "{a:1}" ["a" =: s "AB"] "A", 400 | Test "{a:1}" ["a" =: s "%B"] "%25", 401 | Test "{a:1}" ["a" =: s "\xa0\&B"] "%C2%A0", 402 | Test "{a:1}" ["a" =: s "\xd7ff\&B"] "%ED%9F%BF", 403 | Test "{a:1}" ["a" =: s "\x10000\&B"] "%F0%90%80%80", 404 | Test "{a:1}" ["a" =: l []] "", 405 | Test "{a:1}" ["a" =: l ["AB"]] "AB", 406 | Test "{a:1}" ["a" =: l ["AB", "CD"]] "AB,CD", 407 | Test "{a:1}" ["a" =: d []] "", 408 | Test "{a:1}" ["a" =: d ["AB" =: "12"]] "AB,12", 409 | Test "{a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "AB,12,CD,34", 410 | Test "{a,a}" [] "", 411 | Test "{a,a}" ["a" =: l []] "", 412 | Test "{a,a}" ["a" =: d []] "", 413 | Test "{a,a}" ["a" =: s ""] ",", 414 | Test "{a,b}" ["a" =: s ""] "", 415 | Test "{a,b}" ["b" =: s ""] "", 416 | Test "{%aa}" ["%aa" =: s "A"] "A", 417 | Test "{%aa}" ["%aa" =: l ["A", "B"]] "A,B", 418 | Test "{%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "A,1,B,2", 419 | Test "{%aa*}" ["%aa" =: s "A"] "A", 420 | Test "{%aa*}" ["%aa" =: l ["A", "B"]] "A,B", 421 | Test "{%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "A=1,B=2", 422 | Test "{+a}" [] "", 423 | Test "{+a}" ["a" =: l []] "", 424 | Test "{+a}" ["a" =: d []] "", 425 | Test "{+a}" ["a" =: s ""] "", 426 | Test "{+a}" ["a" =: s "A"] "A", 427 | Test "{+a}" ["a" =: s "~"] "~", 428 | Test "{+a}" ["a" =: s "%"] "%25", 429 | Test "{+a}" ["a" =: s "?"] "?", 430 | Test "{+a}" ["a" =: s "&"] "&", 431 | Test "{+a}" ["a" =: s "\xa0"] "%C2%A0", 432 | Test "{+a}" ["a" =: s "\xd7ff"] "%ED%9F%BF", 433 | Test "{+a}" ["a" =: s "\x10000"] "%F0%90%80%80", 434 | Test "{+a}" ["a" =: l ["A"]] "A", 435 | Test "{+a}" ["a" =: l ["A", "B"]] "A,B", 436 | Test "{+a}" ["a" =: l ["%"]] "%25", 437 | Test "{+a}" ["a" =: l ["\xa0"]] "%C2%A0", 438 | Test "{+a}" ["a" =: d ["A" =: "1"]] "A,1", 439 | Test "{+a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "A,1,B,2", 440 | Test "{+a}" ["a" =: d ["A" =: "%"]] "A,%25", 441 | Test "{+a}" ["a" =: d ["A" =: "\xa0"]] "A,%C2%A0", 442 | Test "{+a}" ["a" =: d ["%" =: "1"]] "%25,1", 443 | Test "{+a*}" [] "", 444 | Test "{+a*}" ["a" =: s ""] "", 445 | Test "{+a*}" ["a" =: s "A"] "A", 446 | Test "{+a*}" ["a" =: l []] "", 447 | Test "{+a*}" ["a" =: l ["A"]] "A", 448 | Test "{+a*}" ["a" =: l ["A", "B"]] "A,B", 449 | Test "{+a*}" ["a" =: d []] "", 450 | Test "{+a*}" ["a" =: d ["A" =: "1"]] "A=1", 451 | Test "{+a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "A=1,B=2", 452 | Test "{+a:1}" [] "", 453 | Test "{+a:1}" ["a" =: s ""] "", 454 | Test "{+a:1}" ["a" =: s "A"] "A", 455 | Test "{+a:1}" ["a" =: s "AB"] "A", 456 | Test "{+a:1}" ["a" =: s "%B"] "%25", 457 | Test "{+a:1}" ["a" =: s "\xa0\&B"] "%C2%A0", 458 | Test "{+a:1}" ["a" =: s "\xd7ff\&B"] "%ED%9F%BF", 459 | Test "{+a:1}" ["a" =: s "\x10000\&B"] "%F0%90%80%80", 460 | Test "{+a:1}" ["a" =: l []] "", 461 | Test "{+a:1}" ["a" =: l ["AB"]] "AB", 462 | Test "{+a:1}" ["a" =: l ["AB", "CD"]] "AB,CD", 463 | Test "{+a:1}" ["a" =: d []] "", 464 | Test "{+a:1}" ["a" =: d ["AB" =: "12"]] "AB,12", 465 | Test "{+a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "AB,12,CD,34", 466 | Test "{+a,a}" [] "", 467 | Test "{+a,a}" ["a" =: l []] "", 468 | Test "{+a,a}" ["a" =: d []] "", 469 | Test "{+a,a}" ["a" =: s ""] ",", 470 | Test "{+a,b}" ["a" =: s ""] "", 471 | Test "{+a,b}" ["b" =: s ""] "", 472 | Test "{+%aa}" ["%aa" =: s "A"] "A", 473 | Test "{+%aa}" ["%aa" =: l ["A", "B"]] "A,B", 474 | Test "{+%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "A,1,B,2", 475 | Test "{+%aa*}" ["%aa" =: s "A"] "A", 476 | Test "{+%aa*}" ["%aa" =: l ["A", "B"]] "A,B", 477 | Test "{+%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "A=1,B=2", 478 | Test "{#a}" [] "", 479 | Test "{#a}" ["a" =: l []] "", 480 | Test "{#a}" ["a" =: d []] "", 481 | Test "{#a}" ["a" =: s ""] "#", 482 | Test "{#a}" ["a" =: s "A"] "#A", 483 | Test "{#a}" ["a" =: s "~"] "#~", 484 | Test "{#a}" ["a" =: s "%"] "#%25", 485 | Test "{#a}" ["a" =: s "?"] "#?", 486 | Test "{#a}" ["a" =: s "&"] "#&", 487 | Test "{#a}" ["a" =: s "\xa0"] "#%C2%A0", 488 | Test "{#a}" ["a" =: s "\xd7ff"] "#%ED%9F%BF", 489 | Test "{#a}" ["a" =: s "\x10000"] "#%F0%90%80%80", 490 | Test "{#a}" ["a" =: l ["A"]] "#A", 491 | Test "{#a}" ["a" =: l ["A", "B"]] "#A,B", 492 | Test "{#a}" ["a" =: l ["%"]] "#%25", 493 | Test "{#a}" ["a" =: l ["\xa0"]] "#%C2%A0", 494 | Test "{#a}" ["a" =: d ["A" =: "1"]] "#A,1", 495 | Test "{#a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "#A,1,B,2", 496 | Test "{#a}" ["a" =: d ["A" =: "%"]] "#A,%25", 497 | Test "{#a}" ["a" =: d ["A" =: "\xa0"]] "#A,%C2%A0", 498 | Test "{#a}" ["a" =: d ["%" =: "1"]] "#%25,1", 499 | Test "{#a*}" [] "", 500 | Test "{#a*}" ["a" =: s ""] "#", 501 | Test "{#a*}" ["a" =: s "A"] "#A", 502 | Test "{#a*}" ["a" =: l []] "", 503 | Test "{#a*}" ["a" =: l ["A"]] "#A", 504 | Test "{#a*}" ["a" =: l ["A", "B"]] "#A,B", 505 | Test "{#a*}" ["a" =: d []] "", 506 | Test "{#a*}" ["a" =: d ["A" =: "1"]] "#A=1", 507 | Test "{#a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "#A=1,B=2", 508 | Test "{#a:1}" [] "", 509 | Test "{#a:1}" ["a" =: s ""] "#", 510 | Test "{#a:1}" ["a" =: s "A"] "#A", 511 | Test "{#a:1}" ["a" =: s "AB"] "#A", 512 | Test "{#a:1}" ["a" =: s "%B"] "#%25", 513 | Test "{#a:1}" ["a" =: s "\xa0\&B"] "#%C2%A0", 514 | Test "{#a:1}" ["a" =: s "\xd7ff\&B"] "#%ED%9F%BF", 515 | Test "{#a:1}" ["a" =: s "\x10000\&B"] "#%F0%90%80%80", 516 | Test "{#a:1}" ["a" =: l []] "", 517 | Test "{#a:1}" ["a" =: l ["AB"]] "#AB", 518 | Test "{#a:1}" ["a" =: l ["AB", "CD"]] "#AB,CD", 519 | Test "{#a:1}" ["a" =: d []] "", 520 | Test "{#a:1}" ["a" =: d ["AB" =: "12"]] "#AB,12", 521 | Test "{#a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "#AB,12,CD,34", 522 | Test "{#a,a}" [] "", 523 | Test "{#a,a}" ["a" =: l []] "", 524 | Test "{#a,a}" ["a" =: d []] "", 525 | Test "{#a,a}" ["a" =: s ""] "#,", 526 | Test "{#a,b}" ["a" =: s ""] "#", 527 | Test "{#a,b}" ["b" =: s ""] "#", 528 | Test "{#%aa}" ["%aa" =: s "A"] "#A", 529 | Test "{#%aa}" ["%aa" =: l ["A", "B"]] "#A,B", 530 | Test "{#%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "#A,1,B,2", 531 | Test "{#%aa*}" ["%aa" =: s "A"] "#A", 532 | Test "{#%aa*}" ["%aa" =: l ["A", "B"]] "#A,B", 533 | Test "{#%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "#A=1,B=2", 534 | Test "{.a}" [] "", 535 | Test "{.a}" ["a" =: l []] "", 536 | Test "{.a}" ["a" =: d []] "", 537 | Test "{.a}" ["a" =: s ""] ".", 538 | Test "{.a}" ["a" =: s "A"] ".A", 539 | Test "{.a}" ["a" =: s "~"] ".~", 540 | Test "{.a}" ["a" =: s "%"] ".%25", 541 | Test "{.a}" ["a" =: s "?"] ".%3F", 542 | Test "{.a}" ["a" =: s "&"] ".%26", 543 | Test "{.a}" ["a" =: s "\xa0"] ".%C2%A0", 544 | Test "{.a}" ["a" =: s "\xd7ff"] ".%ED%9F%BF", 545 | Test "{.a}" ["a" =: s "\x10000"] ".%F0%90%80%80", 546 | Test "{.a}" ["a" =: l ["A"]] ".A", 547 | Test "{.a}" ["a" =: l ["A", "B"]] ".A,B", 548 | Test "{.a}" ["a" =: l ["%"]] ".%25", 549 | Test "{.a}" ["a" =: l ["\xa0"]] ".%C2%A0", 550 | Test "{.a}" ["a" =: d ["A" =: "1"]] ".A,1", 551 | Test "{.a}" ["a" =: d ["A" =: "1", "B" =: "2"]] ".A,1,B,2", 552 | Test "{.a}" ["a" =: d ["A" =: "%"]] ".A,%25", 553 | Test "{.a}" ["a" =: d ["A" =: "\xa0"]] ".A,%C2%A0", 554 | Test "{.a}" ["a" =: d ["%" =: "1"]] ".%25,1", 555 | Test "{.a*}" [] "", 556 | Test "{.a*}" ["a" =: s ""] ".", 557 | Test "{.a*}" ["a" =: s "A"] ".A", 558 | Test "{.a*}" ["a" =: l []] "", 559 | Test "{.a*}" ["a" =: l ["A"]] ".A", 560 | Test "{.a*}" ["a" =: l ["A", "B"]] ".A.B", 561 | Test "{.a*}" ["a" =: d []] "", 562 | Test "{.a*}" ["a" =: d ["A" =: "1"]] ".A=1", 563 | Test "{.a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] ".A=1.B=2", 564 | Test "{.a:1}" [] "", 565 | Test "{.a:1}" ["a" =: s ""] ".", 566 | Test "{.a:1}" ["a" =: s "A"] ".A", 567 | Test "{.a:1}" ["a" =: s "AB"] ".A", 568 | Test "{.a:1}" ["a" =: s "%B"] ".%25", 569 | Test "{.a:1}" ["a" =: s "\xa0\&B"] ".%C2%A0", 570 | Test "{.a:1}" ["a" =: s "\xd7ff\&B"] ".%ED%9F%BF", 571 | Test "{.a:1}" ["a" =: s "\x10000\&B"] ".%F0%90%80%80", 572 | Test "{.a:1}" ["a" =: l []] "", 573 | Test "{.a:1}" ["a" =: l ["AB"]] ".AB", 574 | Test "{.a:1}" ["a" =: l ["AB", "CD"]] ".AB,CD", 575 | Test "{.a:1}" ["a" =: d []] "", 576 | Test "{.a:1}" ["a" =: d ["AB" =: "12"]] ".AB,12", 577 | Test "{.a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] ".AB,12,CD,34", 578 | Test "{.a,a}" [] "", 579 | Test "{.a,a}" ["a" =: l []] "", 580 | Test "{.a,a}" ["a" =: d []] "", 581 | Test "{.a,a}" ["a" =: s ""] "..", 582 | Test "{.a,b}" ["a" =: s ""] ".", 583 | Test "{.a,b}" ["b" =: s ""] ".", 584 | Test "{.%aa}" ["%aa" =: s "A"] ".A", 585 | Test "{.%aa}" ["%aa" =: l ["A", "B"]] ".A,B", 586 | Test "{.%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] ".A,1,B,2", 587 | Test "{.%aa*}" ["%aa" =: s "A"] ".A", 588 | Test "{.%aa*}" ["%aa" =: l ["A", "B"]] ".A.B", 589 | Test "{.%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] ".A=1.B=2", 590 | Test "{/a}" [] "", 591 | Test "{/a}" ["a" =: l []] "", 592 | Test "{/a}" ["a" =: d []] "", 593 | Test "{/a}" ["a" =: s ""] "/", 594 | Test "{/a}" ["a" =: s "A"] "/A", 595 | Test "{/a}" ["a" =: s "~"] "/~", 596 | Test "{/a}" ["a" =: s "%"] "/%25", 597 | Test "{/a}" ["a" =: s "?"] "/%3F", 598 | Test "{/a}" ["a" =: s "&"] "/%26", 599 | Test "{/a}" ["a" =: s "\xa0"] "/%C2%A0", 600 | Test "{/a}" ["a" =: s "\xd7ff"] "/%ED%9F%BF", 601 | Test "{/a}" ["a" =: s "\x10000"] "/%F0%90%80%80", 602 | Test "{/a}" ["a" =: l ["A"]] "/A", 603 | Test "{/a}" ["a" =: l ["A", "B"]] "/A,B", 604 | Test "{/a}" ["a" =: l ["%"]] "/%25", 605 | Test "{/a}" ["a" =: l ["\xa0"]] "/%C2%A0", 606 | Test "{/a}" ["a" =: d ["A" =: "1"]] "/A,1", 607 | Test "{/a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "/A,1,B,2", 608 | Test "{/a}" ["a" =: d ["A" =: "%"]] "/A,%25", 609 | Test "{/a}" ["a" =: d ["A" =: "\xa0"]] "/A,%C2%A0", 610 | Test "{/a}" ["a" =: d ["%" =: "1"]] "/%25,1", 611 | Test "{/a*}" [] "", 612 | Test "{/a*}" ["a" =: s ""] "/", 613 | Test "{/a*}" ["a" =: s "A"] "/A", 614 | Test "{/a*}" ["a" =: l []] "", 615 | Test "{/a*}" ["a" =: l ["A"]] "/A", 616 | Test "{/a*}" ["a" =: l ["A", "B"]] "/A/B", 617 | Test "{/a*}" ["a" =: d []] "", 618 | Test "{/a*}" ["a" =: d ["A" =: "1"]] "/A=1", 619 | Test "{/a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "/A=1/B=2", 620 | Test "{/a:1}" [] "", 621 | Test "{/a:1}" ["a" =: s ""] "/", 622 | Test "{/a:1}" ["a" =: s "A"] "/A", 623 | Test "{/a:1}" ["a" =: s "AB"] "/A", 624 | Test "{/a:1}" ["a" =: s "%B"] "/%25", 625 | Test "{/a:1}" ["a" =: s "\xa0\&B"] "/%C2%A0", 626 | Test "{/a:1}" ["a" =: s "\xd7ff\&B"] "/%ED%9F%BF", 627 | Test "{/a:1}" ["a" =: s "\x10000\&B"] "/%F0%90%80%80", 628 | Test "{/a:1}" ["a" =: l []] "", 629 | Test "{/a:1}" ["a" =: l ["AB"]] "/AB", 630 | Test "{/a:1}" ["a" =: l ["AB", "CD"]] "/AB,CD", 631 | Test "{/a:1}" ["a" =: d []] "", 632 | Test "{/a:1}" ["a" =: d ["AB" =: "12"]] "/AB,12", 633 | Test "{/a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "/AB,12,CD,34", 634 | Test "{/a,a}" [] "", 635 | Test "{/a,a}" ["a" =: l []] "", 636 | Test "{/a,a}" ["a" =: d []] "", 637 | Test "{/a,a}" ["a" =: s ""] "//", 638 | Test "{/a,b}" ["a" =: s ""] "/", 639 | Test "{/a,b}" ["b" =: s ""] "/", 640 | Test "{/%aa}" ["%aa" =: s "A"] "/A", 641 | Test "{/%aa}" ["%aa" =: l ["A", "B"]] "/A,B", 642 | Test "{/%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "/A,1,B,2", 643 | Test "{/%aa*}" ["%aa" =: s "A"] "/A", 644 | Test "{/%aa*}" ["%aa" =: l ["A", "B"]] "/A/B", 645 | Test "{/%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "/A=1/B=2", 646 | Test "{;a}" [] "", 647 | Test "{;a}" ["a" =: l []] "", 648 | Test "{;a}" ["a" =: d []] "", 649 | Test "{;a}" ["a" =: s ""] ";a", 650 | Test "{;a}" ["a" =: s "A"] ";a=A", 651 | Test "{;a}" ["a" =: s "~"] ";a=~", 652 | Test "{;a}" ["a" =: s "%"] ";a=%25", 653 | Test "{;a}" ["a" =: s "?"] ";a=%3F", 654 | Test "{;a}" ["a" =: s "&"] ";a=%26", 655 | Test "{;a}" ["a" =: s "\xa0"] ";a=%C2%A0", 656 | Test "{;a}" ["a" =: s "\xd7ff"] ";a=%ED%9F%BF", 657 | Test "{;a}" ["a" =: s "\x10000"] ";a=%F0%90%80%80", 658 | Test "{;a}" ["a" =: l ["A"]] ";a=A", 659 | Test "{;a}" ["a" =: l ["A", "B"]] ";a=A,B", 660 | Test "{;a}" ["a" =: l ["%"]] ";a=%25", 661 | Test "{;a}" ["a" =: l ["\xa0"]] ";a=%C2%A0", 662 | Test "{;a}" ["a" =: d ["A" =: "1"]] ";a=A,1", 663 | Test "{;a}" ["a" =: d ["A" =: "1", "B" =: "2"]] ";a=A,1,B,2", 664 | Test "{;a}" ["a" =: d ["A" =: "%"]] ";a=A,%25", 665 | Test "{;a}" ["a" =: d ["A" =: "\xa0"]] ";a=A,%C2%A0", 666 | Test "{;a}" ["a" =: d ["%" =: "1"]] ";a=%25,1", 667 | Test "{;a*}" [] "", 668 | Test "{;a*}" ["a" =: s ""] ";a", 669 | Test "{;a*}" ["a" =: s "A"] ";a=A", 670 | Test "{;a*}" ["a" =: l []] "", 671 | Test "{;a*}" ["a" =: l ["A"]] ";a=A", 672 | Test "{;a*}" ["a" =: l ["A", "B"]] ";a=A;a=B", 673 | Test "{;a*}" ["a" =: d []] "", 674 | Test "{;a*}" ["a" =: d ["A" =: "1"]] ";A=1", 675 | Test "{;a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] ";A=1;B=2", 676 | Test "{;a:1}" [] "", 677 | Test "{;a:1}" ["a" =: s ""] ";a", 678 | Test "{;a:1}" ["a" =: s "A"] ";a=A", 679 | Test "{;a:1}" ["a" =: s "AB"] ";a=A", 680 | Test "{;a:1}" ["a" =: s "%B"] ";a=%25", 681 | Test "{;a:1}" ["a" =: s "\xa0\&B"] ";a=%C2%A0", 682 | Test "{;a:1}" ["a" =: s "\xd7ff\&B"] ";a=%ED%9F%BF", 683 | Test "{;a:1}" ["a" =: s "\x10000\&B"] ";a=%F0%90%80%80", 684 | Test "{;a:1}" ["a" =: l []] "", 685 | Test "{;a:1}" ["a" =: l ["AB"]] ";a=AB", 686 | Test "{;a:1}" ["a" =: l ["AB", "CD"]] ";a=AB,CD", 687 | Test "{;a:1}" ["a" =: d []] "", 688 | Test "{;a:1}" ["a" =: d ["AB" =: "12"]] ";a=AB,12", 689 | Test "{;a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] ";a=AB,12,CD,34", 690 | Test "{;a,a}" [] "", 691 | Test "{;a,a}" ["a" =: l []] "", 692 | Test "{;a,a}" ["a" =: d []] "", 693 | Test "{;a,a}" ["a" =: s ""] ";a;a", 694 | Test "{;a,b}" ["a" =: s ""] ";a", 695 | Test "{;a,b}" ["b" =: s ""] ";b", 696 | Test "{;%aa}" ["%aa" =: s "A"] ";%aa=A", 697 | Test "{;%aa}" ["%aa" =: l ["A", "B"]] ";%aa=A,B", 698 | Test "{;%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] ";%aa=A,1,B,2", 699 | Test "{;%aa*}" ["%aa" =: s "A"] ";%aa=A", 700 | Test "{;%aa*}" ["%aa" =: l ["A", "B"]] ";%aa=A;%aa=B", 701 | Test "{;%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] ";A=1;B=2", 702 | Test "{?a}" [] "", 703 | Test "{?a}" ["a" =: l []] "", 704 | Test "{?a}" ["a" =: d []] "", 705 | Test "{?a}" ["a" =: s ""] "?a=", 706 | Test "{?a}" ["a" =: s "A"] "?a=A", 707 | Test "{?a}" ["a" =: s "~"] "?a=~", 708 | Test "{?a}" ["a" =: s "%"] "?a=%25", 709 | Test "{?a}" ["a" =: s "?"] "?a=%3F", 710 | Test "{?a}" ["a" =: s "&"] "?a=%26", 711 | Test "{?a}" ["a" =: s "\xa0"] "?a=%C2%A0", 712 | Test "{?a}" ["a" =: s "\xd7ff"] "?a=%ED%9F%BF", 713 | Test "{?a}" ["a" =: s "\x10000"] "?a=%F0%90%80%80", 714 | Test "{?a}" ["a" =: l ["A"]] "?a=A", 715 | Test "{?a}" ["a" =: l ["A", "B"]] "?a=A,B", 716 | Test "{?a}" ["a" =: l ["%"]] "?a=%25", 717 | Test "{?a}" ["a" =: l ["\xa0"]] "?a=%C2%A0", 718 | Test "{?a}" ["a" =: d ["A" =: "1"]] "?a=A,1", 719 | Test "{?a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "?a=A,1,B,2", 720 | Test "{?a}" ["a" =: d ["A" =: "%"]] "?a=A,%25", 721 | Test "{?a}" ["a" =: d ["A" =: "\xa0"]] "?a=A,%C2%A0", 722 | Test "{?a}" ["a" =: d ["%" =: "1"]] "?a=%25,1", 723 | Test "{?a*}" [] "", 724 | Test "{?a*}" ["a" =: s ""] "?a=", 725 | Test "{?a*}" ["a" =: s "A"] "?a=A", 726 | Test "{?a*}" ["a" =: l []] "", 727 | Test "{?a*}" ["a" =: l ["A"]] "?a=A", 728 | Test "{?a*}" ["a" =: l ["A", "B"]] "?a=A&a=B", 729 | Test "{?a*}" ["a" =: d []] "", 730 | Test "{?a*}" ["a" =: d ["A" =: "1"]] "?A=1", 731 | Test "{?a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "?A=1&B=2", 732 | Test "{?a:1}" [] "", 733 | Test "{?a:1}" ["a" =: s ""] "?a=", 734 | Test "{?a:1}" ["a" =: s "A"] "?a=A", 735 | Test "{?a:1}" ["a" =: s "AB"] "?a=A", 736 | Test "{?a:1}" ["a" =: s "%B"] "?a=%25", 737 | Test "{?a:1}" ["a" =: s "\xa0\&B"] "?a=%C2%A0", 738 | Test "{?a:1}" ["a" =: s "\xd7ff\&B"] "?a=%ED%9F%BF", 739 | Test "{?a:1}" ["a" =: s "\x10000\&B"] "?a=%F0%90%80%80", 740 | Test "{?a:1}" ["a" =: l []] "", 741 | Test "{?a:1}" ["a" =: l ["AB"]] "?a=AB", 742 | Test "{?a:1}" ["a" =: l ["AB", "CD"]] "?a=AB,CD", 743 | Test "{?a:1}" ["a" =: d []] "", 744 | Test "{?a:1}" ["a" =: d ["AB" =: "12"]] "?a=AB,12", 745 | Test "{?a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "?a=AB,12,CD,34", 746 | Test "{?a,a}" [] "", 747 | Test "{?a,a}" ["a" =: l []] "", 748 | Test "{?a,a}" ["a" =: d []] "", 749 | Test "{?a,a}" ["a" =: s ""] "?a=&a=", 750 | Test "{?a,b}" ["a" =: s ""] "?a=", 751 | Test "{?a,b}" ["b" =: s ""] "?b=", 752 | Test "{?%aa}" ["%aa" =: s "A"] "?%aa=A", 753 | Test "{?%aa}" ["%aa" =: l ["A", "B"]] "?%aa=A,B", 754 | Test "{?%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "?%aa=A,1,B,2", 755 | Test "{?%aa*}" ["%aa" =: s "A"] "?%aa=A", 756 | Test "{?%aa*}" ["%aa" =: l ["A", "B"]] "?%aa=A&%aa=B", 757 | Test "{?%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "?A=1&B=2", 758 | Test "{&a}" [] "", 759 | Test "{&a}" ["a" =: l []] "", 760 | Test "{&a}" ["a" =: d []] "", 761 | Test "{&a}" ["a" =: s ""] "&a=", 762 | Test "{&a}" ["a" =: s "A"] "&a=A", 763 | Test "{&a}" ["a" =: s "~"] "&a=~", 764 | Test "{&a}" ["a" =: s "%"] "&a=%25", 765 | Test "{&a}" ["a" =: s "?"] "&a=%3F", 766 | Test "{&a}" ["a" =: s "&"] "&a=%26", 767 | Test "{&a}" ["a" =: s "\xa0"] "&a=%C2%A0", 768 | Test "{&a}" ["a" =: s "\xd7ff"] "&a=%ED%9F%BF", 769 | Test "{&a}" ["a" =: s "\x10000"] "&a=%F0%90%80%80", 770 | Test "{&a}" ["a" =: l ["A"]] "&a=A", 771 | Test "{&a}" ["a" =: l ["A", "B"]] "&a=A,B", 772 | Test "{&a}" ["a" =: l ["%"]] "&a=%25", 773 | Test "{&a}" ["a" =: l ["\xa0"]] "&a=%C2%A0", 774 | Test "{&a}" ["a" =: d ["A" =: "1"]] "&a=A,1", 775 | Test "{&a}" ["a" =: d ["A" =: "1", "B" =: "2"]] "&a=A,1,B,2", 776 | Test "{&a}" ["a" =: d ["A" =: "%"]] "&a=A,%25", 777 | Test "{&a}" ["a" =: d ["A" =: "\xa0"]] "&a=A,%C2%A0", 778 | Test "{&a}" ["a" =: d ["%" =: "1"]] "&a=%25,1", 779 | Test "{&a*}" [] "", 780 | Test "{&a*}" ["a" =: s ""] "&a=", 781 | Test "{&a*}" ["a" =: s "A"] "&a=A", 782 | Test "{&a*}" ["a" =: l []] "", 783 | Test "{&a*}" ["a" =: l ["A"]] "&a=A", 784 | Test "{&a*}" ["a" =: l ["A", "B"]] "&a=A&a=B", 785 | Test "{&a*}" ["a" =: d []] "", 786 | Test "{&a*}" ["a" =: d ["A" =: "1"]] "&A=1", 787 | Test "{&a*}" ["a" =: d ["A" =: "1", "B" =: "2"]] "&A=1&B=2", 788 | Test "{&a:1}" [] "", 789 | Test "{&a:1}" ["a" =: s ""] "&a=", 790 | Test "{&a:1}" ["a" =: s "A"] "&a=A", 791 | Test "{&a:1}" ["a" =: s "AB"] "&a=A", 792 | Test "{&a:1}" ["a" =: s "%B"] "&a=%25", 793 | Test "{&a:1}" ["a" =: s "\xa0\&B"] "&a=%C2%A0", 794 | Test "{&a:1}" ["a" =: s "\xd7ff\&B"] "&a=%ED%9F%BF", 795 | Test "{&a:1}" ["a" =: s "\x10000\&B"] "&a=%F0%90%80%80", 796 | Test "{&a:1}" ["a" =: l []] "", 797 | Test "{&a:1}" ["a" =: l ["AB"]] "&a=AB", 798 | Test "{&a:1}" ["a" =: l ["AB", "CD"]] "&a=AB,CD", 799 | Test "{&a:1}" ["a" =: d []] "", 800 | Test "{&a:1}" ["a" =: d ["AB" =: "12"]] "&a=AB,12", 801 | Test "{&a:1}" ["a" =: d ["AB" =: "12", "CD" =: "34"]] "&a=AB,12,CD,34", 802 | Test "{&a,a}" [] "", 803 | Test "{&a,a}" ["a" =: l []] "", 804 | Test "{&a,a}" ["a" =: d []] "", 805 | Test "{&a,a}" ["a" =: s ""] "&a=&a=", 806 | Test "{&a,b}" ["a" =: s ""] "&a=", 807 | Test "{&a,b}" ["b" =: s ""] "&b=", 808 | Test "{&%aa}" ["%aa" =: s "A"] "&%aa=A", 809 | Test "{&%aa}" ["%aa" =: l ["A", "B"]] "&%aa=A,B", 810 | Test "{&%aa}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "&%aa=A,1,B,2", 811 | Test "{&%aa*}" ["%aa" =: s "A"] "&%aa=A", 812 | Test "{&%aa*}" ["%aa" =: l ["A", "B"]] "&%aa=A&%aa=B", 813 | Test "{&%aa*}" ["%aa" =: d ["A" =: "1", "B" =: "2"]] "&A=1&B=2" 814 | ], 815 | let values = ["%AA" =: s "1", "%Aa" =: s "2", "%aA" =: s "3", "%aa" =: s "4"] 816 | in [ Test "{%AA}" values "1", 817 | Test "{%Aa}" values "2", 818 | Test "{%aA}" values "3", 819 | Test "{%aa}" values "4" 820 | -- This comment forces Brittany to use a multi-line layout. 821 | ], 822 | let values = ["a" =: s "abcdefghijklmnopqrstuvwxyz"] 823 | in [ Test "{a:1}" values "a", 824 | Test "{a:5}" values "abcde", 825 | Test "{a:10}" values "abcdefghij", 826 | Test "{a:15}" values "abcdefghijklmno", 827 | Test "{a:20}" values "abcdefghijklmnopqrst", 828 | Test "{a:25}" values "abcdefghijklmnopqrstuvwxy", 829 | Test "{a:26}" values "abcdefghijklmnopqrstuvwxyz", 830 | Test "{a:30}" values "abcdefghijklmnopqrstuvwxyz" 831 | ], 832 | let values = ["a" =: l []] 833 | in [ Test "{a}" values "", 834 | Test "{+a}" values "", 835 | Test "{#a}" values "", 836 | Test "{.a}" values "", 837 | Test "{/a}" values "", 838 | Test "{;a}" values "", 839 | Test "{?a}" values "", 840 | Test "{&a}" values "" 841 | ], 842 | let values = ["a" =: d []] 843 | in [ Test "{a}" values "", 844 | Test "{+a}" values "", 845 | Test "{#a}" values "", 846 | Test "{.a}" values "", 847 | Test "{/a}" values "", 848 | Test "{;a}" values "", 849 | Test "{?a}" values "", 850 | Test "{&a}" values "" 851 | ], 852 | let values = ["a" =: s ""] 853 | in [ Test "{a}" values "", 854 | Test "{+a}" values "", 855 | Test "{#a}" values "#", 856 | Test "{.a}" values ".", 857 | Test "{/a}" values "/", 858 | Test "{;a}" values ";a", 859 | Test "{?a}" values "?a=", 860 | Test "{&a}" values "&a=" 861 | ], 862 | let values = ["a" =: s "A"] 863 | in [ Test "{a}" values "A", 864 | Test "{+a}" values "A", 865 | Test "{#a}" values "#A", 866 | Test "{.a}" values ".A", 867 | Test "{/a}" values "/A", 868 | Test "{;a}" values ";a=A", 869 | Test "{?a}" values "?a=A", 870 | Test "{&a}" values "&a=A" 871 | ], 872 | let values = ["b" =: s "B"] 873 | in [ Test "{a,b}" values "B", 874 | Test "{+a,b}" values "B", 875 | Test "{#a,b}" values "#B", 876 | Test "{.a,b}" values ".B", 877 | Test "{/a,b}" values "/B", 878 | Test "{;a,b}" values ";b=B", 879 | Test "{?a,b}" values "?b=B", 880 | Test "{&a,b}" values "&b=B" 881 | ], 882 | let values = ["a" =: s ""] 883 | in [ Test "{a,a}" values ",", 884 | Test "{+a,a}" values ",", 885 | Test "{#a,a}" values "#,", 886 | Test "{.a,a}" values "..", 887 | Test "{/a,a}" values "//", 888 | Test "{;a,a}" values ";a;a", 889 | Test "{?a,a}" values "?a=&a=", 890 | Test "{&a,a}" values "&a=&a=" 891 | ], 892 | let values = ["a" =: s "A", "b" =: s "B"] 893 | in [ Test "{a,b}" values "A,B", 894 | Test "{+a,b}" values "A,B", 895 | Test "{#a,b}" values "#A,B", 896 | Test "{.a,b}" values ".A.B", 897 | Test "{/a,b}" values "/A/B", 898 | Test "{;a,b}" values ";a=A;b=B", 899 | Test "{?a,b}" values "?a=A&b=B", 900 | Test "{&a,b}" values "&a=A&b=B" 901 | ], 902 | let values = ["a" =: d ["K! \xa0\xd7ff\x10000" =: "V! \xa0\xd7ff\x10000"]] 903 | in [ Test "{a*}" values "K%21%20%C2%A0%ED%9F%BF%F0%90%80%80=V%21%20%C2%A0%ED%9F%BF%F0%90%80%80", 904 | 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", 905 | 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", 906 | 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", 907 | 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", 908 | 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", 909 | 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", 910 | 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" 911 | ], 912 | let values = 913 | [ "empty" =: s "", 914 | "hello" =: s "Hello World!", 915 | "keys" =: d ["semi" =: ";", "dot" =: ".", "comma" =: ","], 916 | "list" =: l ["red", "green", "blue"], 917 | "path" =: s "/foo/bar", 918 | "var" =: s "value", 919 | "x" =: s "1024", 920 | "y" =: s "768" 921 | ] 922 | in [ Test "{var}" values "value", 923 | Test "{hello}" values "Hello%20World%21", 924 | Test "{+var}" values "value", 925 | Test "{+hello}" values "Hello%20World!", 926 | Test "{+path}/here" values "/foo/bar/here", 927 | Test "here?ref={+path}" values "here?ref=/foo/bar", 928 | Test "X{#var}" values "X#value", 929 | Test "X{#hello}" values "X#Hello%20World!", 930 | Test "map?{x,y}" values "map?1024,768", 931 | Test "{x,hello,y}" values "1024,Hello%20World%21,768", 932 | Test "{+x,hello,y}" values "1024,Hello%20World!,768", 933 | Test "{+path,x}/here" values "/foo/bar,1024/here", 934 | Test "{#x,hello,y}" values "#1024,Hello%20World!,768", 935 | Test "{#path,x}/here" values "#/foo/bar,1024/here", 936 | Test "X{.var}" values "X.value", 937 | Test "X{.x,y}" values "X.1024.768", 938 | Test "{/var}" values "/value", 939 | Test "{/var,x}/here" values "/value/1024/here", 940 | Test "{;x,y}" values ";x=1024;y=768", 941 | Test "{;x,y,empty}" values ";x=1024;y=768;empty", 942 | Test "{?x,y}" values "?x=1024&y=768", 943 | Test "{?x,y,empty}" values "?x=1024&y=768&empty=", 944 | Test "?fixed=yes{&x}" values "?fixed=yes&x=1024", 945 | Test "{&x,y,empty}" values "&x=1024&y=768&empty=", 946 | Test "{var:3}" values "val", 947 | Test "{var:30}" values "value", 948 | Test "{list}" values "red,green,blue", 949 | Test "{list*}" values "red,green,blue", 950 | Test "{keys}" values "comma,%2C,dot,.,semi,%3B", 951 | Test "{keys*}" values "comma=%2C,dot=.,semi=%3B", 952 | Test "{+path:6}/here" values "/foo/b/here", 953 | Test "{+list}" values "red,green,blue", 954 | Test "{+list*}" values "red,green,blue", 955 | Test "{+keys}" values "comma,,,dot,.,semi,;", 956 | Test "{+keys*}" values "comma=,,dot=.,semi=;", 957 | Test "{#path:6}/here" values "#/foo/b/here", 958 | Test "{#list}" values "#red,green,blue", 959 | Test "{#list*}" values "#red,green,blue", 960 | Test "{#keys}" values "#comma,,,dot,.,semi,;", 961 | Test "{#keys*}" values "#comma=,,dot=.,semi=;", 962 | Test "X{.var:3}" values "X.val", 963 | Test "X{.list}" values "X.red,green,blue", 964 | Test "X{.list*}" values "X.red.green.blue", 965 | Test "X{.keys}" values "X.comma,%2C,dot,.,semi,%3B", 966 | Test "X{.keys*}" values "X.comma=%2C.dot=..semi=%3B", 967 | Test "{/var:1,var}" values "/v/value", 968 | Test "{/list}" values "/red,green,blue", 969 | Test "{/list*}" values "/red/green/blue", 970 | Test "{/list*,path:4}" values "/red/green/blue/%2Ffoo", 971 | Test "{/keys}" values "/comma,%2C,dot,.,semi,%3B", 972 | Test "{/keys*}" values "/comma=%2C/dot=./semi=%3B", 973 | Test "{;hello:5}" values ";hello=Hello", 974 | Test "{;list}" values ";list=red,green,blue", 975 | Test "{;list*}" values ";list=red;list=green;list=blue", 976 | Test "{;keys}" values ";keys=comma,%2C,dot,.,semi,%3B", 977 | Test "{;keys*}" values ";comma=%2C;dot=.;semi=%3B", 978 | Test "{?var:3}" values "?var=val", 979 | Test "{?list}" values "?list=red,green,blue", 980 | Test "{?list*}" values "?list=red&list=green&list=blue", 981 | Test "{?keys}" values "?keys=comma,%2C,dot,.,semi,%3B", 982 | Test "{?keys*}" values "?comma=%2C&dot=.&semi=%3B", 983 | Test "{&var:3}" values "&var=val", 984 | Test "{&list}" values "&list=red,green,blue", 985 | Test "{&list*}" values "&list=red&list=green&list=blue", 986 | Test "{&keys}" values "&keys=comma,%2C,dot,.,semi,%3B", 987 | Test "{&keys*}" values "&comma=%2C&dot=.&semi=%3B" 988 | ], 989 | let values = ["var" =: s "value", "semi" =: s ";"] 990 | in [ Test "{var}" values "value", 991 | Test "{var:20}" values "value", 992 | Test "{var:3}" values "val", 993 | Test "{semi}" values "%3B", 994 | Test "{semi:2}" values "%3B" 995 | -- This comment forces Brittany to use a multi-line layout. 996 | ], 997 | let values = ["year" =: l ["1965", "2000", "2012"], "dom" =: l ["example", "com"]] 998 | in [ Test "find{?year*}" values "find?year=1965&year=2000&year=2012", 999 | Test "www{.dom*}" values "www.example.com" 1000 | -- This comment forces Brittany to use a multi-line layout. 1001 | ], 1002 | let values = 1003 | [ "base" =: s "http://example.com/home/", 1004 | "count" =: l ["one", "two", "three"], 1005 | "dom" =: l ["example", "com"], 1006 | "dub" =: s "me/too", 1007 | "empty_keys" =: d [], 1008 | "empty" =: s "", 1009 | "half" =: s "50%", 1010 | "hello" =: s "Hello World!", 1011 | "keys" =: d ["semi" =: ";", "dot" =: ".", "comma" =: ","], 1012 | "list" =: l ["red", "green", "blue"], 1013 | "path" =: s "/foo/bar", 1014 | "v" =: s "6", 1015 | "var" =: s "value", 1016 | "who" =: s "fred", 1017 | "x" =: s "1024", 1018 | "y" =: s "768" 1019 | ] 1020 | in [ Test "{count}" values "one,two,three", 1021 | Test "{count*}" values "one,two,three", 1022 | Test "{/count}" values "/one,two,three", 1023 | Test "{/count*}" values "/one/two/three", 1024 | Test "{;count}" values ";count=one,two,three", 1025 | Test "{;count*}" values ";count=one;count=two;count=three", 1026 | Test "{?count}" values "?count=one,two,three", 1027 | Test "{?count*}" values "?count=one&count=two&count=three", 1028 | Test "{&count*}" values "&count=one&count=two&count=three", 1029 | Test "{var}" values "value", 1030 | Test "{hello}" values "Hello%20World%21", 1031 | Test "{half}" values "50%25", 1032 | Test "O{empty}X" values "OX", 1033 | Test "O{undef}X" values "OX", 1034 | Test "{x,y}" values "1024,768", 1035 | Test "{x,hello,y}" values "1024,Hello%20World%21,768", 1036 | Test "?{x,empty}" values "?1024,", 1037 | Test "?{x,undef}" values "?1024", 1038 | Test "?{undef,y}" values "?768", 1039 | Test "{var:3}" values "val", 1040 | Test "{var:30}" values "value", 1041 | Test "{list}" values "red,green,blue", 1042 | Test "{list*}" values "red,green,blue", 1043 | Test "{keys}" values "comma,%2C,dot,.,semi,%3B", 1044 | Test "{keys*}" values "comma=%2C,dot=.,semi=%3B", 1045 | Test "{+var}" values "value", 1046 | Test "{+hello}" values "Hello%20World!", 1047 | Test "{+half}" values "50%25", 1048 | Test "{base}index" values "http%3A%2F%2Fexample.com%2Fhome%2Findex", 1049 | Test "{+base}index" values "http://example.com/home/index", 1050 | Test "O{+empty}X" values "OX", 1051 | Test "O{+undef}X" values "OX", 1052 | Test "{+path}/here" values "/foo/bar/here", 1053 | Test "here?ref={+path}" values "here?ref=/foo/bar", 1054 | Test "up{+path}{var}/here" values "up/foo/barvalue/here", 1055 | Test "{+x,hello,y}" values "1024,Hello%20World!,768", 1056 | Test "{+path,x}/here" values "/foo/bar,1024/here", 1057 | Test "{+path:6}/here" values "/foo/b/here", 1058 | Test "{+list}" values "red,green,blue", 1059 | Test "{+list*}" values "red,green,blue", 1060 | Test "{+keys}" values "comma,,,dot,.,semi,;", 1061 | Test "{+keys*}" values "comma=,,dot=.,semi=;", 1062 | Test "{#var}" values "#value", 1063 | Test "{#hello}" values "#Hello%20World!", 1064 | Test "{#half}" values "#50%25", 1065 | Test "foo{#empty}" values "foo#", 1066 | Test "foo{#undef}" values "foo", 1067 | Test "{#x,hello,y}" values "#1024,Hello%20World!,768", 1068 | Test "{#path,x}/here" values "#/foo/bar,1024/here", 1069 | Test "{#path:6}/here" values "#/foo/b/here", 1070 | Test "{#list}" values "#red,green,blue", 1071 | Test "{#list*}" values "#red,green,blue", 1072 | Test "{#keys}" values "#comma,,,dot,.,semi,;", 1073 | Test "{#keys*}" values "#comma=,,dot=.,semi=;", 1074 | Test "{.who}" values ".fred", 1075 | Test "{.who,who}" values ".fred.fred", 1076 | Test "{.half,who}" values ".50%25.fred", 1077 | Test "www{.dom*}" values "www.example.com", 1078 | Test "X{.var}" values "X.value", 1079 | Test "X{.empty}" values "X.", 1080 | Test "X{.undef}" values "X", 1081 | Test "X{.var:3}" values "X.val", 1082 | Test "X{.list}" values "X.red,green,blue", 1083 | Test "X{.list*}" values "X.red.green.blue", 1084 | Test "X{.keys}" values "X.comma,%2C,dot,.,semi,%3B", 1085 | Test "X{.keys*}" values "X.comma=%2C.dot=..semi=%3B", 1086 | Test "X{.empty_keys}" values "X", 1087 | Test "X{.empty_keys*}" values "X", 1088 | Test "{/who}" values "/fred", 1089 | Test "{/who,who}" values "/fred/fred", 1090 | Test "{/half,who}" values "/50%25/fred", 1091 | Test "{/who,dub}" values "/fred/me%2Ftoo", 1092 | Test "{/var}" values "/value", 1093 | Test "{/var,empty}" values "/value/", 1094 | Test "{/var,undef}" values "/value", 1095 | Test "{/var,x}/here" values "/value/1024/here", 1096 | Test "{/var:1,var}" values "/v/value", 1097 | Test "{/list}" values "/red,green,blue", 1098 | Test "{/list*}" values "/red/green/blue", 1099 | Test "{/list*,path:4}" values "/red/green/blue/%2Ffoo", 1100 | Test "{/keys}" values "/comma,%2C,dot,.,semi,%3B", 1101 | Test "{/keys*}" values "/comma=%2C/dot=./semi=%3B", 1102 | Test "{;who}" values ";who=fred", 1103 | Test "{;half}" values ";half=50%25", 1104 | Test "{;empty}" values ";empty", 1105 | Test "{;v,empty,who}" values ";v=6;empty;who=fred", 1106 | Test "{;v,bar,who}" values ";v=6;who=fred", 1107 | Test "{;x,y}" values ";x=1024;y=768", 1108 | Test "{;x,y,empty}" values ";x=1024;y=768;empty", 1109 | Test "{;x,y,undef}" values ";x=1024;y=768", 1110 | Test "{;hello:5}" values ";hello=Hello", 1111 | Test "{;list}" values ";list=red,green,blue", 1112 | Test "{;list*}" values ";list=red;list=green;list=blue", 1113 | Test "{;keys}" values ";keys=comma,%2C,dot,.,semi,%3B", 1114 | Test "{;keys*}" values ";comma=%2C;dot=.;semi=%3B", 1115 | Test "{?who}" values "?who=fred", 1116 | Test "{?half}" values "?half=50%25", 1117 | Test "{?x,y}" values "?x=1024&y=768", 1118 | Test "{?x,y,empty}" values "?x=1024&y=768&empty=", 1119 | Test "{?x,y,undef}" values "?x=1024&y=768", 1120 | Test "{?var:3}" values "?var=val", 1121 | Test "{?list}" values "?list=red,green,blue", 1122 | Test "{?list*}" values "?list=red&list=green&list=blue", 1123 | Test "{?keys}" values "?keys=comma,%2C,dot,.,semi,%3B", 1124 | Test "{?keys*}" values "?comma=%2C&dot=.&semi=%3B", 1125 | Test "{&who}" values "&who=fred", 1126 | Test "{&half}" values "&half=50%25", 1127 | Test "?fixed=yes{&x}" values "?fixed=yes&x=1024", 1128 | Test "{&x,y,empty}" values "&x=1024&y=768&empty=", 1129 | Test "{&x,y,undef}" values "&x=1024&y=768", 1130 | Test "{&var:3}" values "&var=val", 1131 | Test "{&list}" values "&list=red,green,blue", 1132 | Test "{&list*}" values "&list=red&list=green&list=blue", 1133 | Test "{&keys}" values "&keys=comma,%2C,dot,.,semi,%3B", 1134 | Test "{&keys*}" values "&comma=%2C&dot=.&semi=%3B" 1135 | ] 1136 | ] 1137 | 1138 | data Test = Test 1139 | { testInput :: String, 1140 | testValues :: [(String, Burrito.Value)], 1141 | testOutput :: Output 1142 | } 1143 | deriving (Eq, Show) 1144 | 1145 | runTest :: Test -> Hspec.Expectation 1146 | runTest test = 1147 | case (Burrito.parse $ testInput test, unwrapOutput $ testOutput test) of 1148 | (Nothing, Nothing) -> pure () 1149 | (Nothing, Just _) -> Hspec.expectationFailure "should have parsed" 1150 | (Just _, Nothing) -> Hspec.expectationFailure "should not have parsed" 1151 | (Just template, Just expected) -> do 1152 | let values = testValues test 1153 | actual = Burrito.expand values template 1154 | actual `Hspec.shouldBe` expected 1155 | Burrito.parse (show template) `Hspec.shouldBe` Just template 1156 | let relevant = 1157 | List.sort $ keepRelevant (templateVariables template) values 1158 | Monad.when (isMatchable template relevant) $ do 1159 | let matches = List.sort <$> Burrito.match expected template 1160 | matches `Hspec.shouldSatisfy` elem relevant 1161 | 1162 | isMatchable :: Template.Template -> [(String, Burrito.Value)] -> Bool 1163 | isMatchable template values = 1164 | (not . any (isAsterisk . Variable.modifier) $ templateVariables template) 1165 | && all (isString . snd) values 1166 | 1167 | isString :: Burrito.Value -> Bool 1168 | isString value = case value of 1169 | Value.String _ -> True 1170 | _ -> False 1171 | 1172 | isAsterisk :: Modifier.Modifier -> Bool 1173 | isAsterisk modifier = case modifier of 1174 | Modifier.Asterisk -> True 1175 | _ -> False 1176 | 1177 | keepRelevant :: 1178 | Set.Set Variable.Variable -> 1179 | [(String, Burrito.Value)] -> 1180 | [(String, Burrito.Value)] 1181 | keepRelevant variables = 1182 | let vs = 1183 | Map.fromListWith 1184 | ( \mx my -> case (mx, my) of 1185 | (Just x, Just y) -> Just $ max x y 1186 | _ -> Nothing 1187 | ) 1188 | . fmap 1189 | ( \v -> 1190 | ( Render.builderToString . Render.name $ Variable.name v, 1191 | case Variable.modifier v of 1192 | Modifier.Colon n -> Just $ MaxLength.count n 1193 | _ -> Nothing 1194 | ) 1195 | ) 1196 | $ Set.toList variables 1197 | in Maybe.mapMaybe $ \(k, v) -> do 1198 | m <- Map.lookup k vs 1199 | pure . (,) k $ case m of 1200 | Nothing -> v 1201 | Just n -> case v of 1202 | Value.String t -> Value.String $ Text.take n t 1203 | _ -> v 1204 | 1205 | templateVariables :: Template.Template -> Set.Set Variable.Variable 1206 | templateVariables = 1207 | Set.fromList 1208 | . concatMap 1209 | ( \token -> case token of 1210 | Token.Expression expression -> 1211 | NonEmpty.toList $ Expression.variables expression 1212 | Token.Literal _ -> [] 1213 | ) 1214 | . Template.tokens 1215 | 1216 | newtype Output = Output 1217 | { unwrapOutput :: Maybe String 1218 | } 1219 | deriving (Eq, Show) 1220 | 1221 | instance String.IsString Output where 1222 | fromString = Output . Just 1223 | 1224 | noParse :: Output 1225 | noParse = Output Nothing 1226 | 1227 | (=:) :: a -> b -> (a, b) 1228 | (=:) = (,) 1229 | 1230 | s :: String -> Burrito.Value 1231 | s = Burrito.stringValue 1232 | 1233 | l :: [String] -> Burrito.Value 1234 | l = Burrito.listValue 1235 | 1236 | d :: [(String, String)] -> Burrito.Value 1237 | d = Burrito.dictionaryValue 1238 | 1239 | newtype Template = Template 1240 | { unwrapTemplate :: Template.Template 1241 | } 1242 | deriving (Eq) 1243 | 1244 | instance Show Template where 1245 | show (Template template) = 1246 | unwords [show $ show template, "{-", show (Template.tokens template), "-}"] 1247 | 1248 | instance QC.Arbitrary Template where 1249 | arbitrary = Template <$> arbitraryTemplate 1250 | shrink = fmap Template . shrinkTemplate . unwrapTemplate 1251 | 1252 | type Shrink a = a -> [a] 1253 | 1254 | arbitraryTemplate :: QC.Gen Template.Template 1255 | arbitraryTemplate = Template.Template . simplify <$> QC.listOf arbitraryToken 1256 | 1257 | shrinkTemplate :: Shrink Template.Template 1258 | shrinkTemplate = 1259 | fmap (Template.Template . simplify) 1260 | . QC.shrinkList shrinkToken 1261 | . Template.tokens 1262 | 1263 | simplify :: [Token.Token] -> [Token.Token] 1264 | simplify tokens = case tokens of 1265 | t1 : t2 : ts -> case (t1, t2) of 1266 | (Token.Literal l1, Token.Literal l2) -> 1267 | simplify $ Token.Literal (appendLiteral l1 l2) : ts 1268 | _ -> t1 : simplify (t2 : ts) 1269 | _ -> tokens 1270 | 1271 | appendLiteral :: Literal.Literal -> Literal.Literal -> Literal.Literal 1272 | appendLiteral x y = 1273 | Literal.Literal $ Literal.characters x <> Literal.characters y 1274 | 1275 | arbitraryToken :: QC.Gen Token.Token 1276 | arbitraryToken = 1277 | QC.oneof 1278 | [ Token.Expression <$> arbitraryExpression, 1279 | Token.Literal <$> arbitraryLiteral 1280 | ] 1281 | 1282 | shrinkToken :: Shrink Token.Token 1283 | shrinkToken x = case x of 1284 | Token.Expression y -> Token.Expression <$> shrinkExpression y 1285 | Token.Literal y -> Token.Literal <$> shrinkLiteral y 1286 | 1287 | arbitraryExpression :: QC.Gen Expression.Expression 1288 | arbitraryExpression = 1289 | Expression.Expression 1290 | <$> arbitraryOperator 1291 | <*> arbitraryNonEmpty arbitraryVariable 1292 | 1293 | shrinkExpression :: Shrink Expression.Expression 1294 | shrinkExpression x = 1295 | uncurry Expression.Expression 1296 | <$> shrinkTuple 1297 | shrinkOperator 1298 | (shrinkNonEmpty shrinkVariable) 1299 | (Expression.operator x, Expression.variables x) 1300 | 1301 | arbitraryOperator :: QC.Gen Operator.Operator 1302 | arbitraryOperator = 1303 | QC.elements 1304 | [ Operator.Ampersand, 1305 | Operator.FullStop, 1306 | Operator.None, 1307 | Operator.NumberSign, 1308 | Operator.PlusSign, 1309 | Operator.QuestionMark, 1310 | Operator.Semicolon, 1311 | Operator.Solidus 1312 | ] 1313 | 1314 | shrinkOperator :: Shrink Operator.Operator 1315 | shrinkOperator x = case x of 1316 | Operator.None -> [] 1317 | _ -> [Operator.None] 1318 | 1319 | arbitraryNonEmpty :: QC.Gen a -> QC.Gen (NonEmpty.NonEmpty a) 1320 | arbitraryNonEmpty g = (NonEmpty.:|) <$> g <*> QC.listOf g 1321 | 1322 | shrinkNonEmpty :: Shrink a -> Shrink (NonEmpty.NonEmpty a) 1323 | shrinkNonEmpty f x = 1324 | uncurry (NonEmpty.:|) 1325 | <$> shrinkTuple f (QC.shrinkList f) (NonEmpty.head x, NonEmpty.tail x) 1326 | 1327 | arbitraryVariable :: QC.Gen Variable.Variable 1328 | arbitraryVariable = Variable.Variable <$> arbitraryName <*> arbitraryModifier 1329 | 1330 | shrinkVariable :: Shrink Variable.Variable 1331 | shrinkVariable variable = 1332 | uncurry Variable.Variable 1333 | <$> shrinkTuple 1334 | shrinkName 1335 | shrinkModifier 1336 | (Variable.name variable, Variable.modifier variable) 1337 | 1338 | shrinkTuple :: Shrink a -> Shrink b -> Shrink (a, b) 1339 | shrinkTuple f g (x, y) = 1340 | fmap (\a -> (a, y)) (f x) <> fmap (\b -> (x, b)) (g y) 1341 | 1342 | arbitraryName :: QC.Gen Name.Name 1343 | arbitraryName = Name.Name <$> arbitraryNonEmpty arbitraryField 1344 | 1345 | shrinkName :: Shrink Name.Name 1346 | shrinkName = fmap Name.Name . shrinkNonEmpty shrinkField . Name.fields 1347 | 1348 | arbitraryField :: QC.Gen Field.Field 1349 | arbitraryField = Field.Field <$> arbitraryNonEmpty arbitraryFieldCharacter 1350 | 1351 | shrinkField :: Shrink Field.Field 1352 | shrinkField = 1353 | fmap Field.Field . shrinkNonEmpty shrinkFieldCharacter . Field.characters 1354 | 1355 | arbitraryFieldCharacter :: QC.Gen (Character.Character Field.Field) 1356 | arbitraryFieldCharacter = 1357 | QC.oneof 1358 | [ Character.Encoded <$> arbitraryDigit <*> arbitraryDigit, 1359 | Character.Unencoded <$> QC.suchThat QC.arbitrary Parse.isFieldCharacter 1360 | ] 1361 | 1362 | shrinkFieldCharacter :: Shrink (Character.Character Field.Field) 1363 | shrinkFieldCharacter x = case x of 1364 | Character.Encoded y z -> 1365 | uncurry Character.Encoded <$> shrinkTuple shrinkDigit shrinkDigit (y, z) 1366 | Character.Unencoded y -> 1367 | fmap Character.Unencoded . filter Parse.isFieldCharacter $ QC.shrink y 1368 | 1369 | arbitraryDigit :: QC.Gen Digit.Digit 1370 | arbitraryDigit = 1371 | QC.oneof 1372 | [ pure Digit.Ox0, 1373 | pure Digit.Ox1, 1374 | pure Digit.Ox2, 1375 | pure Digit.Ox3, 1376 | pure Digit.Ox4, 1377 | pure Digit.Ox5, 1378 | pure Digit.Ox6, 1379 | pure Digit.Ox7, 1380 | pure Digit.Ox8, 1381 | pure Digit.Ox9, 1382 | Digit.OxA <$> arbitraryCase, 1383 | Digit.OxB <$> arbitraryCase, 1384 | Digit.OxC <$> arbitraryCase, 1385 | Digit.OxD <$> arbitraryCase, 1386 | Digit.OxE <$> arbitraryCase, 1387 | Digit.OxF <$> arbitraryCase 1388 | ] 1389 | 1390 | shrinkDigit :: Shrink Digit.Digit 1391 | shrinkDigit x = case x of 1392 | Digit.Ox0 -> [] 1393 | Digit.Ox1 -> [Digit.Ox0] 1394 | Digit.Ox2 -> [Digit.Ox0] 1395 | Digit.Ox3 -> [Digit.Ox0] 1396 | Digit.Ox4 -> [Digit.Ox0] 1397 | Digit.Ox5 -> [Digit.Ox0] 1398 | Digit.Ox6 -> [Digit.Ox0] 1399 | Digit.Ox7 -> [Digit.Ox0] 1400 | Digit.Ox8 -> [Digit.Ox0] 1401 | Digit.Ox9 -> [Digit.Ox0] 1402 | Digit.OxA y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y) 1403 | Digit.OxB y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y) 1404 | Digit.OxC y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y) 1405 | Digit.OxD y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y) 1406 | Digit.OxE y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y) 1407 | Digit.OxF y -> Digit.Ox0 : fmap Digit.OxA (shrinkCase y) 1408 | 1409 | arbitraryCase :: QC.Gen Case.Case 1410 | arbitraryCase = QC.elements [Case.Lower, Case.Upper] 1411 | 1412 | shrinkCase :: Shrink Case.Case 1413 | shrinkCase x = case x of 1414 | Case.Lower -> [] 1415 | Case.Upper -> [Case.Lower] 1416 | 1417 | arbitraryModifier :: QC.Gen Modifier.Modifier 1418 | arbitraryModifier = 1419 | QC.oneof 1420 | [ pure Modifier.Asterisk, 1421 | Modifier.Colon <$> arbitraryMaxLength, 1422 | pure Modifier.None 1423 | ] 1424 | 1425 | shrinkModifier :: Shrink Modifier.Modifier 1426 | shrinkModifier x = case x of 1427 | Modifier.Asterisk -> [Modifier.None] 1428 | Modifier.Colon y -> Modifier.None : fmap Modifier.Colon (shrinkMaxLength y) 1429 | Modifier.None -> [] 1430 | 1431 | arbitraryMaxLength :: QC.Gen MaxLength.MaxLength 1432 | arbitraryMaxLength = 1433 | MaxLength.MaxLength <$> QC.suchThat QC.arbitrary Parse.isMaxLength 1434 | 1435 | shrinkMaxLength :: Shrink MaxLength.MaxLength 1436 | shrinkMaxLength = 1437 | fmap MaxLength.MaxLength 1438 | . filter Parse.isMaxLength 1439 | . QC.shrink 1440 | . MaxLength.count 1441 | 1442 | arbitraryLiteral :: QC.Gen Literal.Literal 1443 | arbitraryLiteral = 1444 | Literal.Literal <$> arbitraryNonEmpty arbitraryLiteralCharacter 1445 | 1446 | shrinkLiteral :: Shrink Literal.Literal 1447 | shrinkLiteral = 1448 | fmap Literal.Literal 1449 | . shrinkNonEmpty shrinkLiteralCharacter 1450 | . Literal.characters 1451 | 1452 | arbitraryLiteralCharacter :: QC.Gen (Character.Character Literal.Literal) 1453 | arbitraryLiteralCharacter = 1454 | QC.oneof 1455 | [ Character.Encoded <$> arbitraryDigit <*> arbitraryDigit, 1456 | Character.Unencoded <$> QC.suchThat QC.arbitrary Parse.isLiteralCharacter 1457 | ] 1458 | 1459 | shrinkLiteralCharacter :: Shrink (Character.Character Literal.Literal) 1460 | shrinkLiteralCharacter x = case x of 1461 | Character.Encoded y z -> 1462 | uncurry Character.Encoded <$> shrinkTuple shrinkDigit shrinkDigit (y, z) 1463 | Character.Unencoded y -> 1464 | fmap Character.Unencoded . filter Parse.isLiteralCharacter $ QC.shrink y 1465 | --------------------------------------------------------------------------------