packages feed

interp (empty) → 1.0.0.0

raw patch · 16 files changed

+736/−0 lines, 16 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, containers, hspec, interp, megaparsec, mtl, optparse-applicative, parser-combinators, random-fu, rvar, semigroups, text, transformers, unordered-containers, vector

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for interp++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright sam raker (c) 2019++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of sam raker nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,91 @@+# interp++A randomized text-interpolation tool inspired by [Tracery](https://tracery.io), in Haskell.++### Installation+#### Prerequisites+  * Haskell+  * [Stack](https://www.haskellstack.org), unless you can work Cabal yourself++Clone this repo, `cd` into it, and then:+  * if you want to use the CLI, run `stack install`+  * if you want to use it as a library, just run `stack build`++#### Development+Fork this repo, make some changes, run `stack test` to make sure everything's ok, then (if you want) submit a PR. Thank you!++### Use+#### CLI+```+Usage: interp (--substitutions ARG | SUBST) (--interpolations ARG | INTERP)+  Randomly interpolate values into a template++Available options:+  --substitutions ARG      JSON file containing map of substitutions+  SUBST                    JSON file containing map of substitutions+  --interpolations ARG     file containing text to interpolate+  INTERP                   file containing text to interpolate+  -h,--help                Show this help text+```++`substitutions` is the (path to the) file containing the JSON map of substitutions. `interpolations` is (path to the) file containing the text to interpolate. See below for more on both.++#### Library+`Data.Text.Interp.Interpolate.interp` is probably what you'll want to use. It takes a `Subst` object (usually parsed from JSON, but you can make one yourself) and a `NonEmpty` list of `IText`s. Check out `Data.Text.Interp.Types` for more on those types.++### Syntax+In both substitution and interpolation files, keys can be comprised of any letter or number, or the characters "\_" or "-". Spaces and other punctuation are not allowed and will give you Problems.+#### Substitutions+```+{"coolThings": [+   {"name": "computers",+    "reason": "you can do haskell on them"+   },+   {"name": "dogs",+    "reason": "they're good"+   }+ ],+ "badThings": [+  {"name": "flat tires",+   "reason": "they make you late"+  },+  {"name": "jobs",+   "reason": "they make you tired"+  }+ ],+ "sam": {+   "likes": "computers"+}+```+The substitutions file is a JSON file. JSON maps are treated how one would expect. The values in a JSON array are chosen among randomly (but see also below on binding). Regular values are treated regularly, but note:+  * because of how JSON is parsed, _all_ numeric literals are treated as floating-point, so a `1` will be interpolated as `"1.0"`. If this is an issue, just wrap your numbers in quotes and they'll be treated as strings.+  * `null` values are explicitly forbidden; empty lists and maps should be avoided.++#### Interpolations+`{{ ... }}` sets off a thing to interpolate. Keys, corresponding to keys in the substitutions file, can be period-separated to 'dig into' nested maps. Thus, in the example above, `sam.likes` will be have the value `"computers"`.++##### Binding+Within braces, values can be bound to variables using the syntax `(varToBind#keys.to.bind).other.keys`. When a variable is bound, references to that variable will be interpolated with that same value. Thus, given the interpolation template+```+{{ (cool#coolThings).name }} are cool because {{ cool.reason }}. {{ (bad#badThings).name }} are bad because {{ bad.reason }}.+```+`cool` in the first interpolation will be the same as `cool` in the second. Note that keys within the parentheses are part of the binding, while those after the closing parentheses are interpolated normally--`{{ (cool#coolThings).name }}` will be either `"computers"` or `"dogs"`, while `{{ cool.reason }}` will be `"you can do haskell on them"` or `"they're good"`, depending on which sub-map was randomly chosen to be bound to `cool`.++Binding is primarily useful for map values. It's possible to bind arrays, but since array elements are chosen randomly evey time, there's not much of a point. Similarly, there's no real difference between binding a simple value and just using the full path to it, unless you're trying to save characters.+++### Etc.+#### To-Do+ - [ ] Haddock documentation/comments+ - [x] Add positional args in CLI+ - [ ] Other formats (e.g. YAML) for substitutions file+ - [ ] More features+   - [ ] Preprocessing (prepending "a(n)", title casing, etc.)+   - [ ] Other stuff??+     - [ ] Check Tracery documentation for stuff to steal+   - [ ] Web interface??+ - [ ] Come up with a better name+ - [ ] Publish to Hackage++#### Contributing+PRs, issues, feature requests, etc. are always more than welcome! Feel free to hit me up on [twitter](https://twitter.com/swizzard) or email me at sam dot raker at gmail dot com. Let me know if you like this, hate this, wish it were better, wish it were worse, whatever.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE LambdaCase #-}+module Main where++import Control.Monad.Except+import Data.Text (unpack, Text)+import Options.Applicative+import System.IO++import Data.Text.Interp (doInterp, opts, Input(..))+++run :: Input -> IO (Either Text Text)+run (Input sf tf) = runExceptT $ doInterp sf tf+++main :: IO ()+main = execParser opts >>= run >>= \case+    (Left e) -> hPutStrLn stderr $ unpack e+    (Right v) -> hPutStrLn stdout $ unpack v
+ interp.cabal view
@@ -0,0 +1,117 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.1.+--+-- see: https://github.com/sol/hpack+--+-- hash: 2c5ae1e8028bb10c6d701cd2ccc86981da63658513595f95c99c55d09c26074e++name:           interp+version:        1.0.0.0+synopsis:       Tracery-like randomized text interpolation+description:    Please see the README on GitHub at <https://github.com/swizzard/interp#readme>+category:       Text, Interpolation+homepage:       https://github.com/swizzard/interp#readme+bug-reports:    https://github.com/swizzard/interp/issues+author:         sam raker+maintainer:     sam.raker@gmail.com+copyright:      (c) 2019 sam raker+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    ChangeLog.md++source-repository head+  type: git+  location: https://github.com/swizzard/interp++library+  exposed-modules:+      Data.Text.Interp+      Data.Text.Interp.CLI+      Data.Text.Interp.Get+      Data.Text.Interp.Interpolate+      Data.Text.Interp.Parse+      Data.Text.Interp.Types+  other-modules:+      Paths_interp+  hs-source-dirs:+      src+  build-depends:+      aeson+    , base >=4.7 && <5+    , bytestring+    , containers+    , hspec+    , megaparsec+    , mtl+    , optparse-applicative+    , parser-combinators+    , random-fu+    , rvar+    , semigroups+    , text+    , transformers+    , unordered-containers+    , vector+  default-language: Haskell2010++executable interp+  main-is: Main.hs+  other-modules:+      Paths_interp+  hs-source-dirs:+      app+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson+    , base >=4.7 && <5+    , bytestring+    , containers+    , hspec+    , interp+    , megaparsec+    , mtl+    , optparse-applicative+    , parser-combinators+    , random-fu+    , rvar+    , semigroups+    , text+    , transformers+    , unordered-containers+    , vector+  default-language: Haskell2010++test-suite interp-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      GetSpec+      InterpolateSpec+      ParseSpec+      Paths_interp+  hs-source-dirs:+      test+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson+    , base >=4.7 && <5+    , bytestring+    , containers+    , hspec+    , interp+    , megaparsec+    , mtl+    , optparse-applicative+    , parser-combinators+    , random-fu+    , rvar+    , semigroups+    , text+    , transformers+    , unordered-containers+    , vector+  default-language: Haskell2010
+ src/Data/Text/Interp.hs view
@@ -0,0 +1,34 @@+module Data.Text.Interp+  ( opts+  , doInterp+  , Input(..)+  ) where++import Control.Monad.Except+import Control.Monad.Trans (lift)+import Data.Aeson+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as T+import System.IO (FilePath)+import Text.Megaparsec++import Data.Text.Interp.CLI (Input(..), opts)+import Data.Text.Interp.Interpolate (interp)+import Data.Text.Interp.Parse (itP)+import Data.Text.Interp.Types (I)+++-- | Generate interpolated text within a `Data.Text.Interp.Types.I` monad+doInterp :: FilePath -- ^ path to substitutions file+         -> FilePath -- ^ path to interpolations file+         -> I Text   -- ^ resulting interpolated text+doInterp sf tf = do+  i <- parse itP "" <$> T.strip <$> (lift $ T.readFile tf)+  case i of+    (Left e) -> throwError . T.pack $ errorBundlePretty e+    (Right i') -> do+      s <- lift $ eitherDecodeFileStrict sf+      case s of+        (Left e) -> throwError $ T.pack e+        (Right s') -> interp s' i'
+ src/Data/Text/Interp/CLI.hs view
@@ -0,0 +1,37 @@+module Data.Text.Interp.CLI+  ( Input(..)+  , opts+  ) where++import Data.Semigroup ((<>))+import Options.Applicative+import System.IO (FilePath)+++-- | Record to hold CLI options+data Input = Input {+             substF :: FilePath -- ^ path to the substitutions file+           , interpF :: FilePath -- ^ path to the interpolations file+           }++-- | CLI parser for `Input`+input :: Parser Input+input = Input <$> (+        (strOption $ long "substitutions"+         <> help "JSON file containing map of substitutions")+        <|>+        (strArgument $ metavar "SUBST"+          <> help "JSON file containing map of substitutions")+        ) <*> (+        (strOption $ long "interpolations"+           <> help "file containing text to interpolate")+        <|>+         (strArgument $ metavar "INTERP"+           <> help "file containing text to interpolate"))++-- | ParserInfo for `Input`+opts :: ParserInfo Input+opts = info (input <**> helper)+          (fullDesc+          <> progDesc "Randomly interpolate values into a template"+          <> header "interp")
+ src/Data/Text/Interp/Get.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}+module Data.Text.Interp.Get+  ( get+  ) where++import Control.Monad.Except+import qualified Data.Map.Strict as M+import Data.Random (MonadRandom, randomElement)+import Data.RVar (sampleRVar)+import Data.Text (Text, append)++import Data.Text.Interp.Types+++-- | Randomly retrieve a value out of a `Data.Text.Interp.Types.Subst` mapping+get :: Subst -- ^ source mapping+    -> [Key] -- ^ a list of `Data.Text.Interp.Types.Key`s leading to the value+    -> BindingMap -- ^ the `Data.Text.Interp.Types.BindingMap` to check for bound values in+    -> I Subst -- ^ retrieved value+get v@(SubstV _) [] m = return v+get (SubstV _) xs _ = throwError "too many keys"+get m@(SubstM _) [] _ = return $ m+get (SubstM m) (k:ks) bm = case M.lookup k bm of+                             Nothing -> do+                               res <- g k m+                               get res ks bm+                             (Just s') -> get s' ks bm++-- | Helper function that digs one layer down in a `Data.Text.Interp.Types.Subst` mapping+g :: Key -> (M.Map Key Subst) -> I Subst+g k m = case M.lookup k m of+  Nothing -> throwError $ "bad key: " `append` (unKey k)+  (Just (SubstL xs)) -> lift $ sampleRVar (randomElement xs)+  (Just v) -> return v
+ src/Data/Text/Interp/Interpolate.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE OverloadedStrings #-}+module Data.Text.Interp.Interpolate where++import Control.Monad (foldM)+import Control.Monad.Except+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as NE+import qualified Data.Map.Strict as M+import Data.Text (Text, append)++import Data.Text.Interp.Get+import Data.Text.Interp.Types+++-- | Record holding intermediary interpolation state+data IState = IState {+              acc :: Text -- ^ interpolated text so far+            , bm :: BindingMap -- ^ mapping of bound variables+            }++newIState :: IState+newIState = IState "" M.empty++-- | Build interpolated text out of a `Data.Text.Interp.Types.Subst` mapping and+-- a list of segments to interpolate+interp :: Subst -- ^ mapping to get substitutions out of+       -> NonEmpty IText -- ^ lits of segments to interpolate+       -> I Text -- ^ final interpolation+interp s its = acc <$> foldM (interp' s) newIState its++-- | Interpolate a single segment+interp' :: Subst -- ^ mapping+        -> IState -- ^ intermediary state+        -> IText -- ^ segment to interpolate+        -> I IState -- ^ updated state, after the segment's been interpolated+interp' _ (IState a m) (RawText t) = return $ IState (a `append` t) m+interp' s (IState a m) (ToInterpolate Nothing p) = do+  res <- get s p m+  case res of+    (SubstM _) -> throwError "too few keys"+    (SubstL _) -> throwError "too few keys"+    (SubstV x) -> return $ IState (a `append` x) m+interp' s (IState a m) (ToInterpolate (Just (Binding n bp)) p) = do+  toBind <- get s (NE.toList bp) m+  res <- get toBind p m+  case res of+    (SubstM _) -> throwError "too few keys"+    (SubstL _) -> throwError "too few keys"+    (SubstV x) -> return $ IState (a `append` x) $ M.insert n toBind m
+ src/Data/Text/Interp/Parse.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.Text.Interp.Parse where++import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as NE+import Data.Text (Text, pack)+import Data.Void (Void)+import Text.Megaparsec+import Text.Megaparsec.Char++import Data.Text.Interp.Types++type Parser = Parsec Void Text++-- | Parse `Data.Text.Interp.Types.IText`s+itP :: Parser (NonEmpty IText)+itP = NE.fromList <$> (some $ try $ toInterpolateP <|> rawTextP)++interpStart = "{{"+interpStop = "}}"++-- | parse raw text (i.e., text without interpolations)+rawTextP :: Parser IText+rawTextP = RawText <$> takeWhile1P (Just "raw text") (\c -> not (c `elem` ['}', '{']))++-- | parse text that needs interpolation+toInterpolateP :: Parser IText+toInterpolateP = between (string interpStart >> space)+                         (space >> string interpStop)+                 interP++-- | parse interpolation instructions (with or without binding)+interP :: Parser IText+interP = try $ bindP <|> keyP++-- | parse interpolation that needs binding+bindP :: Parser IText+bindP = do+  char '('+  bk <- key <* char '#'+  bp <- NE.fromList <$> keys <* string ")"+  path <- (optional (char '.')) *> keys+  let b = Just $ Binding bk bp+  return $ ToInterpolate b path++-- | parse interpolation that doesn't need binding+keyP :: Parser IText+keyP = ToInterpolate Nothing <$> keys++-- | parse a single key+key :: Parser Key+key = Key <$> pack <$> some (alphaNumChar <|> satisfy (`elem` ['_', '-'])) <?>+          "keys can only contain letters, numbers, _, and -"++-- | parse a list of keys+keys :: Parser [Key]+keys = sepBy key (char '.')
+ src/Data/Text/Interp/Types.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module Data.Text.Interp.Types+  ( Binding(..)+  , BindingMap+  , I+  , IText(..)+  , Key(..)+  , Subst(..)+  , substm+  ) where++import Control.Monad.Except+import Data.Aeson+import Data.Aeson.Types (Parser)+import qualified Data.HashMap.Strict as HM+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.Map.Strict as M+import Data.Map.Strict (Map)+import Data.String (IsString, fromString)+import Data.Text (Text, pack)+import qualified Data.Vector as V+++newtype Key = Key { unKey :: Text } deriving (Eq, IsString, Monoid, Ord, Semigroup, Show)++-- | The main substitution type, either a list, mapping, or single value+data Subst = SubstL [Subst] -- ^ a list of values, one of which will be randomly selected+           | SubstM (Map Key Subst) -- ^ a mapping of `Key`s to values+           | SubstV Text -- ^ a single value+           deriving (Eq, Show)++instance FromJSON Subst where+  parseJSON (String s) = return $ SubstV s+  parseJSON (Number n) = return $ SubstV . pack . show $ n+  parseJSON (Bool b) = return $ SubstV . pack . show $ b+  parseJSON Null = fail "null not allowed"+  parseJSON (Array a) = SubstL <$> (sequence $ V.toList $ parseJSON <$> a)+  parseJSON (Object o) = SubstM <$> HM.foldlWithKey' g (return M.empty) o where+    g :: Parser (Map Key Subst) -> Text -> Value -> Parser (Map Key Subst)+    -- considered liftM2 but it was hard to read imo+    g p k v = do+      acc <- p+      x <- parseJSON v+      return $ M.insert (Key k) x acc++-- | Helper function to build a `Subst` mapping+substm :: [(Key, Subst)] -> Subst+substm = SubstM . M.fromList++-- | A binding of a `Key` to a path of `Key`s+data Binding = Binding {+               name :: Key+             , pathToBind :: NonEmpty Key+             } deriving (Eq, Show)++-- | A segment of text to interpolate (or not)+data IText = RawText Text -- ^ 'Raw' text that doesn't need interpolation+             -- | Text that requires interpolation+           | ToInterpolate {+                binding :: Maybe Binding -- ^ Optional binding+             ,  path :: [Key] -- ^ Path of `Key`s to the value to interpolate+             } deriving (Eq, Show)++type I = ExceptT Text IO++type BindingMap = M.Map Key Subst
+ test/GetSpec.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings #-}+module GetSpec where++import Control.Monad.Except+import qualified Data.Map.Strict as M+import Test.Hspec++import Data.Text.Interp.Get+import Data.Text.Interp.Types++samFedoraColors :: Subst+samFedoraColors = SubstL $ SubstV <$> ["green", "red", "blue"]++samBallcapColors :: Subst+samBallcapColors = SubstL $ SubstV <$> ["white", "black", "orange"]++testSubst :: Subst+testSubst = substm [+  ("people", SubstL [+    substm [+      ("sam", substm [+        ("hats", SubstL [+          substm [+            ("type", SubstV "fedora"),+            ("color", samFedoraColors)+         ],+          substm [+            ("type", SubstV "ballcap"),+            ("color", samBallcapColors)+         ]+        ]),+        ("shoes", SubstL $ SubstV <$> ["sneakers", "boots"])+      ])+     ]+    ])+   ]++expectedColors = let (SubstL fcs) = samFedoraColors+                     (SubstL bcs) = samBallcapColors+                  in fcs ++ bcs++spec :: Spec+spec = do+  describe "get" $ do+    it "should get a color" $ do+      (Right c) <- runExceptT $+        get testSubst ["people", "sam", "hats", "color"] M.empty+      c `shouldSatisfy` (`elem` expectedColors)++    it "should error on bad key" $ do+      (Left e) <- runExceptT $ get testSubst ["people", "sam", "shirts"] M.empty+      e `shouldBe` "bad key: shirts"++    it "should error on too many keys" $ do+      (Left e) <- runExceptT $+        get testSubst ["people", "sam", "hats", "color", "hue"] M.empty+      e `shouldBe` "too many keys"++    it "should re-use bound results" $ do+      let bm = M.singleton "hats"  $+                  substm [("type", SubstV "fedora"), ("color", samFedoraColors)]+      (Right c) <- runExceptT $ get testSubst ["people", "sam", "hats", "type"] bm+      c `shouldBe` SubstV "fedora"
+ test/InterpolateSpec.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings #-}+module InterpolateSpec where++import Control.Monad.Except+import qualified Data.Map.Strict as M+import Data.List.NonEmpty (NonEmpty(..))+import Data.Text (append)+import Test.Hspec++import Data.Text.Interp.Interpolate+import Data.Text.Interp.Types++fcs = ["green", "red"]+bcs = ["white", "black"]+++samFedoraColors :: Subst+samFedoraColors = SubstL $ SubstV <$> fcs++samBallcapColors :: Subst+samBallcapColors = SubstL $ SubstV <$> bcs++testSubst :: Subst+testSubst = substm [+  ("people", SubstL [+    substm [+      ("sam", substm [+        ("hats", SubstL [+          substm [+            ("type", SubstV "fedora"),+            ("color", samFedoraColors)+         ],+          substm [+            ("type", SubstV "ballcap"),+            ("color", samBallcapColors)+         ]+        ]),+        ("shoes", SubstL $ SubstV <$> ["sneakers", "boots"])+      ])+     ]+    ])+   ]++spec :: Spec+spec = do+  describe "interp" $ do+    it "should interpolate without binding" $ do+      let its = RawText "sam was wearing a " :|+                [ToInterpolate Nothing ["people", "sam", "hats", "type"]]+      (Right res) <- runExceptT $ interp testSubst its+      let expected = ["sam was wearing a fedora", "sam was wearing a ballcap"]+      res `shouldSatisfy` (`elem` expected)++    it "should interpolate with binding" $ do+      let its = RawText "sam was wearing a " :|+                [ToInterpolate (Just (Binding "h" $ "people" :|+                                              ["sam", "hats"])) ["color"],+                 RawText " ",+                 ToInterpolate Nothing ["h", "type"]]+      (Right res) <- runExceptT $ interp testSubst its+      let expected = ["sam was wearing a " `append` fc `append` " fedora" | fc <- fcs] +++                     ["sam was wearing a " `append` bc `append` " ballcap" | bc <- bcs]+      res `shouldSatisfy` (`elem` expected)++    it "should throw an error with too few keys" $ do+      let its = RawText "sam was wearing a  " :|+                [ToInterpolate Nothing ["people", "sam"]]+      (Left e) <- runExceptT $ interp testSubst its+      e `shouldBe` "too few keys"
+ test/ParseSpec.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings #-}+module ParseSpec where++import Data.Either+import Data.Text (Text)+import Data.List.NonEmpty (NonEmpty(..))+import Test.Hspec+import Text.Megaparsec++import Data.Text.Interp.Parse+import Data.Text.Interp.Types+++spec :: Spec+spec = do+  describe "parsing" $ do+    it "should parse a single key" $ do+      let (Right k) = parse key "" "key"+      k `shouldBe` (Key "key")++    it "should parse multiple keys separated by periods" $ do+      let (Right ks) = parse keys "" "foo.bar.baz"+      ks `shouldBe` [Key "foo", Key "bar", Key "baz"]++    it "should create a ToInterpolate without any binding" $ do+      let (Right ti) = parse keyP "" "foo.bar.baz"+      ti `shouldBe` ToInterpolate Nothing [Key "foo", Key "bar", Key "baz"]++    it "should create a ToInterpolate with a binding" $ do+      let (Right ti) = parse bindP "" "(b#foo.bar).baz.quux"+      ti `shouldBe` ToInterpolate (Just $ Binding (Key "b")+                                    (Key "foo" :| [Key "bar"]))+                                    [Key "baz", Key "quux"]++    it "should parse raw text" $ do+      let (Right txt) = parse rawTextP "" "raw text"+      txt `shouldBe` RawText "raw text"++    it "shouldn't permit an empty binding key" $ do+      let res = parse bindP "" "(#foo).bar.baz"+      res `shouldSatisfy` isLeft++    it "shouldn't permit parens without binding stuff" $ do+      let res = parse bindP "" "(foo).bar.baz"+      res `shouldSatisfy` isLeft++    it "should deal properly with errant periods" $ do+      let res = parse bindP "" ".foo.bar"+      res `shouldSatisfy` isLeft+      let res' = parse bindP "" "foo.bar."+      res' `shouldSatisfy` isLeft++    it "should parse a mixture properly" $ do+      let s = "there is some {{ (text#to.parse) }} {{ for.us }} here"+      let (Right res) = parse itP "" s+      let expected = (RawText "there is some ") :|+                      [+                        ToInterpolate (Just $ Binding (Key "text")+                                  (Key "to" :| [Key "parse"])) [],+                        RawText " ",+                        ToInterpolate Nothing [Key "for", Key "us"],+                        RawText " here" ]+      res `shouldBe` expected
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}